github.com/khulnasoft-lab/defsec@v1.0.5-0.20230827010352-5e9f46893d95/pkg/scan/code.go (about) 1 package scan 2 3 import ( 4 "fmt" 5 "io/fs" 6 "path/filepath" 7 "strings" 8 9 defsecTypes "github.com/khulnasoft-lab/defsec/pkg/types" 10 ) 11 12 type Code struct { 13 Lines []Line 14 } 15 16 type Line struct { 17 Number int `json:"Number"` 18 Content string `json:"Content"` 19 IsCause bool `json:"IsCause"` 20 Annotation string `json:"Annotation"` 21 Truncated bool `json:"Truncated"` 22 Highlighted string `json:"Highlighted,omitempty"` 23 FirstCause bool `json:"FirstCause"` 24 LastCause bool `json:"LastCause"` 25 } 26 27 func (c *Code) IsCauseMultiline() bool { 28 var count int 29 for _, line := range c.Lines { 30 if line.IsCause { 31 count++ 32 if count > 1 { 33 return true 34 } 35 } 36 } 37 return false 38 } 39 40 const ( 41 darkTheme = "solarized-dark256" 42 lightTheme = "github" 43 ) 44 45 type codeSettings struct { 46 theme string 47 allowTruncation bool 48 maxLines int 49 includeHighlighted bool 50 } 51 52 var defaultCodeSettings = codeSettings{ 53 theme: darkTheme, 54 allowTruncation: true, 55 maxLines: 10, 56 includeHighlighted: true, 57 } 58 59 type CodeOption func(*codeSettings) 60 61 func OptionCodeWithTheme(theme string) CodeOption { 62 return func(s *codeSettings) { 63 s.theme = theme 64 } 65 } 66 67 func OptionCodeWithDarkTheme() CodeOption { 68 return func(s *codeSettings) { 69 s.theme = darkTheme 70 } 71 } 72 73 func OptionCodeWithLightTheme() CodeOption { 74 return func(s *codeSettings) { 75 s.theme = lightTheme 76 } 77 } 78 79 func OptionCodeWithTruncation(truncate bool) CodeOption { 80 return func(s *codeSettings) { 81 s.allowTruncation = truncate 82 } 83 } 84 85 func OptionCodeWithMaxLines(lines int) CodeOption { 86 return func(s *codeSettings) { 87 s.maxLines = lines 88 } 89 } 90 91 func OptionCodeWithHighlighted(include bool) CodeOption { 92 return func(s *codeSettings) { 93 s.includeHighlighted = include 94 } 95 } 96 97 func validateRange(r defsecTypes.Range) error { 98 if r.GetStartLine() < 0 || r.GetStartLine() > r.GetEndLine() || r.GetEndLine() < 0 { 99 return fmt.Errorf("invalid range: %s", r.String()) 100 } 101 return nil 102 } 103 104 // nolint 105 func (r *Result) GetCode(opts ...CodeOption) (*Code, error) { 106 107 settings := defaultCodeSettings 108 for _, opt := range opts { 109 opt(&settings) 110 } 111 112 srcFS := r.Metadata().Range().GetFS() 113 if srcFS == nil { 114 return nil, fmt.Errorf("code unavailable: result was not mapped to a known filesystem") 115 } 116 117 innerRange := r.Range() 118 outerRange := innerRange 119 metadata := r.Metadata() 120 for { 121 if parent := metadata.Parent(); parent != nil && 122 parent.Range().GetFilename() == metadata.Range().GetFilename() && 123 parent.Range().GetStartLine() > 0 { 124 outerRange = parent.Range() 125 metadata = *parent 126 continue 127 } 128 break 129 } 130 131 if err := validateRange(innerRange); err != nil { 132 return nil, err 133 } 134 if err := validateRange(outerRange); err != nil { 135 return nil, err 136 } 137 138 slashed := filepath.ToSlash(r.fsPath) 139 slashed = strings.TrimPrefix(slashed, "/") 140 141 content, err := fs.ReadFile(srcFS, slashed) 142 if err != nil { 143 return nil, fmt.Errorf("failed to read file from result filesystem (%#v): %w", srcFS, err) 144 } 145 146 hasAnnotation := r.Annotation() != "" 147 148 code := Code{ 149 Lines: nil, 150 } 151 152 rawLines := strings.Split(string(content), "\n") 153 154 var highlightedLines []string 155 if settings.includeHighlighted { 156 highlightedLines = highlight(defsecTypes.CreateFSKey(innerRange.GetFS()), innerRange.GetLocalFilename(), content, settings.theme) 157 if len(highlightedLines) < len(rawLines) { 158 highlightedLines = rawLines 159 } 160 } else { 161 highlightedLines = make([]string, len(rawLines)) 162 } 163 164 if outerRange.GetEndLine()-1 >= len(rawLines) || innerRange.GetStartLine() == 0 { 165 return nil, fmt.Errorf("invalid line number") 166 } 167 168 shrink := settings.allowTruncation && outerRange.LineCount() > (innerRange.LineCount()+10) 169 170 if shrink { 171 172 if outerRange.GetStartLine() < innerRange.GetStartLine() { 173 code.Lines = append( 174 code.Lines, 175 Line{ 176 Content: rawLines[outerRange.GetStartLine()-1], 177 Highlighted: highlightedLines[outerRange.GetStartLine()-1], 178 Number: outerRange.GetStartLine(), 179 }, 180 ) 181 if outerRange.GetStartLine()+1 < innerRange.GetStartLine() { 182 code.Lines = append( 183 code.Lines, 184 Line{ 185 Truncated: true, 186 Number: outerRange.GetStartLine() + 1, 187 }, 188 ) 189 } 190 } 191 192 for lineNo := innerRange.GetStartLine(); lineNo <= innerRange.GetEndLine(); lineNo++ { 193 194 if lineNo-1 >= len(rawLines) || lineNo-1 >= len(highlightedLines) { 195 break 196 } 197 198 line := Line{ 199 Number: lineNo, 200 Content: strings.TrimSuffix(rawLines[lineNo-1], "\r"), 201 Highlighted: strings.TrimSuffix(highlightedLines[lineNo-1], "\r"), 202 IsCause: true, 203 } 204 205 if hasAnnotation && lineNo == innerRange.GetStartLine() { 206 line.Annotation = r.Annotation() 207 } 208 209 code.Lines = append(code.Lines, line) 210 } 211 212 if outerRange.GetEndLine() > innerRange.GetEndLine() { 213 if outerRange.GetEndLine() > innerRange.GetEndLine()+1 { 214 code.Lines = append( 215 code.Lines, 216 Line{ 217 Truncated: true, 218 Number: outerRange.GetEndLine() - 1, 219 }, 220 ) 221 } 222 code.Lines = append( 223 code.Lines, 224 Line{ 225 Content: rawLines[outerRange.GetEndLine()-1], 226 Highlighted: highlightedLines[outerRange.GetEndLine()-1], 227 Number: outerRange.GetEndLine(), 228 }, 229 ) 230 231 } 232 233 } else { 234 for lineNo := outerRange.GetStartLine(); lineNo <= outerRange.GetEndLine(); lineNo++ { 235 236 line := Line{ 237 Number: lineNo, 238 Content: strings.TrimSuffix(rawLines[lineNo-1], "\r"), 239 Highlighted: strings.TrimSuffix(highlightedLines[lineNo-1], "\r"), 240 IsCause: lineNo >= innerRange.GetStartLine() && lineNo <= innerRange.GetEndLine(), 241 } 242 243 if hasAnnotation && lineNo == innerRange.GetStartLine() { 244 line.Annotation = r.Annotation() 245 } 246 247 code.Lines = append(code.Lines, line) 248 } 249 } 250 251 if settings.allowTruncation && len(code.Lines) > settings.maxLines && settings.maxLines > 0 { 252 previouslyTruncated := settings.maxLines-1 > 0 && code.Lines[settings.maxLines-2].Truncated 253 if settings.maxLines-1 > 0 && code.Lines[settings.maxLines-1].LastCause { 254 code.Lines[settings.maxLines-2].LastCause = true 255 } 256 code.Lines[settings.maxLines-1] = Line{ 257 Truncated: true, 258 Number: code.Lines[settings.maxLines-1].Number, 259 } 260 if previouslyTruncated { 261 code.Lines = code.Lines[:settings.maxLines-1] 262 } else { 263 code.Lines = code.Lines[:settings.maxLines] 264 } 265 } 266 267 var first, last bool 268 for i, line := range code.Lines { 269 if line.IsCause && !first { 270 code.Lines[i].FirstCause = true 271 first = true 272 continue 273 } 274 if first && !line.IsCause && i > 0 { 275 code.Lines[i-1].LastCause = true 276 last = true 277 break 278 } 279 } 280 if !last && len(code.Lines) > 0 { 281 code.Lines[len(code.Lines)-1].LastCause = true 282 } 283 284 return &code, nil 285 }