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  }