diff --git a/Makefile b/Makefile index c16cea52..934d4d7b 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ RM = rm -rf GOSEC = /opt/homebrew/bin/gosec # Targets -.phony: all prep run_tests clean tidy install uninstall gosec +.phony: all prep run_tests clean tidy install uninstall gosec gv default: all @@ -60,6 +60,11 @@ uninstall: gosec: $(GOSEC) ./... +gv: out/tmp/diagram.png + +out/tmp/diagram.png: out/tmp/diagram.gv + dot -Tpng $< -o $@ + bin/raa_calc: cmd/raa/main.go $(GO) build $(GOFLAGS) -o $@ $< diff --git a/internal/threagile/root.go b/internal/threagile/root.go index 513fa5ad..3005ce40 100644 --- a/internal/threagile/root.go +++ b/internal/threagile/root.go @@ -33,8 +33,8 @@ Aliases: Examples: {{.Example}}{{end}}{{if .HasAvailableSubCommands}} -Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasHelpSubCommands}} +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Title "help"))}} + {{rpad .Title .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasHelpSubCommands}} Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}} diff --git a/pkg/common/config.go b/pkg/common/config.go index 4ad139e3..9b15f31c 100644 --- a/pkg/common/config.go +++ b/pkg/common/config.go @@ -39,6 +39,8 @@ type Config struct { RiskRulesPlugins []string SkipRiskRules string ExecuteModelMacro string + HideColumns []string + GroupByColumns []string ServerMode bool DiagramDPI int @@ -81,6 +83,8 @@ func (c *Config) Defaults(buildTimestamp string) *Config { TemplateFilename: TemplateFilename, RAAPlugin: RAAPluginName, RiskRulesPlugins: make([]string, 0), + HideColumns: make([]string, 0), + GroupByColumns: make([]string, 0), SkipRiskRules: "", ExecuteModelMacro: "", ServerMode: false, @@ -259,6 +263,12 @@ func (c *Config) Merge(config Config, values map[string]any) { case strings.ToLower("RiskRulesPlugins"): c.RiskRulesPlugins = config.RiskRulesPlugins + case strings.ToLower("HideColumns"): + c.HideColumns = append(c.HideColumns, config.HideColumns...) + + case strings.ToLower("GroupByColumns"): + c.GroupByColumns = append(c.GroupByColumns, config.GroupByColumns...) + case strings.ToLower("SkipRiskRules"): c.SkipRiskRules = config.SkipRiskRules diff --git a/pkg/report/excel-column.go b/pkg/report/excel-column.go new file mode 100644 index 00000000..d71bf692 --- /dev/null +++ b/pkg/report/excel-column.go @@ -0,0 +1,65 @@ +package report + +import ( + "github.com/xuri/excelize/v2" + "strings" +) + +type ExcelColumns map[string]ExcelColumn + +func (what *ExcelColumns) GetColumns() ExcelColumns { + *what = map[string]ExcelColumn{ + "A": {Title: "Severity", Width: 12}, + "B": {Title: "Likelihood", Width: 15}, + "C": {Title: "Impact", Width: 15}, + "D": {Title: "STRIDE", Width: 22}, + "E": {Title: "Function", Width: 16}, + "F": {Title: "CWE", Width: 12}, + "G": {Title: "Risk Category", Width: 50}, + "H": {Title: "Technical Asset", Width: 50}, + "I": {Title: "Communication Link", Width: 50}, + "J": {Title: "RAA %", Width: 10}, + "K": {Title: "Identified Risk", Width: 75}, + "L": {Title: "Action", Width: 45}, + "M": {Title: "Mitigation", Width: 75}, + "N": {Title: "Check", Width: 40}, + "O": {Title: "ID", Width: 10}, + "P": {Title: "Status", Width: 18}, + "Q": {Title: "Justification", Width: 80}, + "R": {Title: "Date", Width: 18}, + "S": {Title: "Checked by", Width: 20}, + "T": {Title: "Ticket", Width: 20}, + } + + return *what +} + +func (what *ExcelColumns) FindColumnNameByTitle(title string) string { + for column, excelColumn := range *what { + if strings.EqualFold(excelColumn.Title, title) { + return column + } + } + + return "" +} + +func (what *ExcelColumns) FindColumnIndexByTitle(title string) int { + for column, excelColumn := range *what { + if strings.EqualFold(excelColumn.Title, title) { + columnNumber, columnNumberError := excelize.ColumnNameToNumber(column) + if columnNumberError != nil { + return -1 + } + + return columnNumber - 1 + } + } + + return -1 +} + +type ExcelColumn struct { + Title string + Width float64 +} diff --git a/pkg/report/excel-style.go b/pkg/report/excel-style.go new file mode 100644 index 00000000..7f36d6e0 --- /dev/null +++ b/pkg/report/excel-style.go @@ -0,0 +1,507 @@ +package report + +import ( + "fmt" + "github.com/threagile/threagile/pkg/security/types" + "github.com/xuri/excelize/v2" + "strings" +) + +type ExcelStyles struct { + severityCriticalBold int + severityCriticalCenter int + severityHighBold int + severityHighCenter int + severityElevatedBold int + severityElevatedCenter int + severityMediumBold int + severityMediumCenter int + severityLowBold int + severityLowCenter int + redCenter int + greenCenter int + blueCenter int + yellowCenter int + orangeCenter int + grayCenter int + blackLeft int + blackLeftBold int + blackCenter int + blackRight int + blackSmall int + graySmall int + blackBold int + mitigation int + headCenter int + headCenterBoldItalic int + headCenterBold int +} + +type styleCreator struct { + ExcelFile *excelize.File + Error error +} + +func (what *styleCreator) Init(excel *excelize.File) *styleCreator { + what.ExcelFile = excel + return what +} + +func (what *styleCreator) NewStyle(style *excelize.Style) int { + if what.Error != nil { + return 0 + } + + var styleID int + styleID, what.Error = what.ExcelFile.NewStyle(style) + + return styleID +} + +func (what *ExcelStyles) Init(excel *excelize.File) (*ExcelStyles, error) { + if excel == nil { + return what, fmt.Errorf("no excel file provided to create styles") + } + + creator := new(styleCreator).Init(excel) + + what.severityCriticalBold = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorCriticalRisk(), + Size: 12, + Bold: true, + }, + }) + + what.severityCriticalCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorCriticalRisk(), + Size: 12, + }, + }) + + what.severityHighBold = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorHighRisk(), + Size: 12, + Bold: true, + }, + }) + + what.severityHighCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorHighRisk(), + Size: 12, + }, + }) + + what.severityElevatedBold = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorElevatedRisk(), + Size: 12, + Bold: true, + }, + }) + + what.severityElevatedCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorElevatedRisk(), + Size: 12, + }, + }) + + what.severityMediumBold = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorMediumRisk(), + Size: 12, + Bold: true, + }, + }) + + what.severityMediumCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorMediumRisk(), + Size: 12, + }, + }) + + what.severityLowBold = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorLowRisk(), + Size: 12, + Bold: true, + }, + }) + + what.severityLowCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorLowRisk(), + Size: 12, + }, + }) + + what.redCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorLowRisk(), + Size: 12, + }, + }) + + what.greenCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorRiskStatusMitigated(), + Size: 12, + }, + }) + + what.blueCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorRiskStatusInProgress(), + Size: 12, + }, + }) + + what.yellowCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorRiskStatusAccepted(), + Size: 12, + }, + }) + + what.orangeCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorRiskStatusInDiscussion(), + Size: 12, + }, + }) + + what.grayCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: rgbHexColorRiskStatusFalsePositive(), + Size: 12, + }, + }) + + what.blackLeft = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "left", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: "#000000", + Size: 12, + }, + }) + + what.blackCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: "#000000", + Size: 12, + }, + }) + + what.blackRight = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "right", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: "#000000", + Size: 12, + }, + }) + + what.blackSmall = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + ShrinkToFit: true, + }, + Font: &excelize.Font{ + Color: "#000000", + Size: 10, + }, + }) + + what.graySmall = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + ShrinkToFit: true, + }, + Font: &excelize.Font{ + Color: rgbHexColorOutOfScope(), + Size: 10, + }, + }) + + what.blackBold = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: "#000000", + Size: 12, + Bold: true, + }, + }) + + what.blackLeftBold = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "left", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: "#000000", + Size: 12, + Bold: true, + }, + }) + + what.mitigation = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + ShrinkToFit: true, + }, + Font: &excelize.Font{ + Color: rgbHexColorRiskStatusMitigated(), + Size: 10, + }, + }) + + what.headCenterBoldItalic = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: "#000000", + Bold: true, + Italic: false, + Size: 14, + }, + Fill: excelize.Fill{ + Type: "pattern", + Color: []string{"#eeeeee"}, + Pattern: 1, + }, + }) + + what.headCenterBold = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: "#000000", + Size: 14, + Bold: true, + }, + Fill: excelize.Fill{ + Type: "pattern", + Color: []string{"#eeeeee"}, + Pattern: 1, + }, + }) + + what.headCenter = creator.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + ShrinkToFit: true, + WrapText: false, + }, + Font: &excelize.Font{ + Color: "#000000", + Size: 14, + }, + Fill: excelize.Fill{ + Type: "pattern", + Color: []string{"#eeeeee"}, + Pattern: 1, + }, + }) + + return what, creator.Error +} + +func (what *ExcelStyles) Get(column string, status types.RiskStatus, severity types.RiskSeverity) int { + switch strings.ToUpper(column) { + case "A", "B", "C", "D", "E", "F": + if !status.IsStillAtRisk() { + return what.blackCenter + } + + switch severity { + case types.CriticalSeverity: + return what.severityCriticalCenter + + case types.HighSeverity: + return what.severityHighCenter + + case types.ElevatedSeverity: + return what.severityElevatedCenter + + case types.MediumSeverity: + return what.severityMediumCenter + + case types.LowSeverity: + return what.severityLowCenter + } + + case "G", "H", "I": + if !status.IsStillAtRisk() { + return what.blackBold + } + + switch severity { + case types.CriticalSeverity: + return what.severityCriticalBold + + case types.HighSeverity: + return what.severityHighBold + + case types.ElevatedSeverity: + return what.severityElevatedBold + + case types.MediumSeverity: + return what.severityMediumBold + + case types.LowSeverity: + return what.severityLowBold + } + + case "J": + return what.blackRight + + case "K": + return what.blackSmall + + case "L", "M", "N": + return what.mitigation + + case "O": + return what.graySmall + + case "P": + switch status { + case types.Unchecked: + return what.redCenter + + case types.Mitigated: + return what.greenCenter + + case types.InProgress: + return what.blueCenter + + case types.Accepted: + return what.yellowCenter + + case types.InDiscussion: + return what.orangeCenter + + case types.FalsePositive: + return what.grayCenter + + default: + return what.blackCenter + } + + case "Q": + return what.blackSmall + + case "R", "S": + return what.blackCenter + + case "T": + return what.blackLeft + } + + return what.blackRight +} diff --git a/pkg/report/excel.go b/pkg/report/excel.go index d1010af8..7580b1be 100644 --- a/pkg/report/excel.go +++ b/pkg/report/excel.go @@ -2,19 +2,22 @@ package report import ( "fmt" + "github.com/shopspring/decimal" + "github.com/threagile/threagile/pkg/common" + "github.com/threagile/threagile/pkg/security/types" + "github.com/xuri/excelize/v2" "sort" "strconv" "strings" - - "github.com/threagile/threagile/pkg/security/types" - "github.com/xuri/excelize/v2" + "unicode/utf8" ) -func WriteRisksExcelToFile(parsedModel *types.ParsedModel, filename string) error { - excelRow := 0 +func WriteRisksExcelToFile(parsedModel *types.ParsedModel, filename string, config *common.Config) error { + columns := new(ExcelColumns).GetColumns() excel := excelize.NewFile() sheetName := parsedModel.Title - err := excel.SetDocProps(&excelize.DocProperties{ + + setDocPropsError := excel.SetDocProps(&excelize.DocProperties{ Category: "Threat Model Risks Summary", ContentStatus: "Final", Creator: parsedModel.Author.Name, @@ -28,20 +31,28 @@ func WriteRisksExcelToFile(parsedModel *types.ParsedModel, filename string) erro Language: "en-US", Version: "1.0.0", }) - if err != nil { - return fmt.Errorf("unable to set doc properties: %w", err) + if setDocPropsError != nil { + return fmt.Errorf("failed to set doc properties: %w", setDocPropsError) + } + + sheetIndex, newSheetError := excel.NewSheet(sheetName) + if newSheetError != nil { + return fmt.Errorf("failed to add sheet: %w", newSheetError) + } + + deleteSheetError := excel.DeleteSheet("Sheet1") + if deleteSheetError != nil { + return fmt.Errorf("failed to delete sheet: %w", deleteSheetError) } - sheetIndex, _ := excel.NewSheet(sheetName) - _ = excel.DeleteSheet("Sheet1") orientation := "landscape" - size := 9 - err = excel.SetPageLayout(sheetName, &excelize.PageLayoutOptions{Orientation: &orientation, Size: &size}) // A4 - if err != nil { - return fmt.Errorf("unable to set page layout: %w", err) + size := 9 // A4 + setPageLayoutError := excel.SetPageLayout(sheetName, &excelize.PageLayoutOptions{Orientation: &orientation, Size: &size}) + if setPageLayoutError != nil { + return fmt.Errorf("unable to set page layout: %w", setPageLayoutError) } - err = excel.SetHeaderFooter(sheetName, &excelize.HeaderFooterOptions{ + setHeaderFooterError := excel.SetHeaderFooter(sheetName, &excelize.HeaderFooterOptions{ DifferentFirst: false, DifferentOddEven: false, OddHeader: "&R&P", @@ -50,635 +61,174 @@ func WriteRisksExcelToFile(parsedModel *types.ParsedModel, filename string) erro EvenFooter: "&L&D&R&T", FirstHeader: `&Threat Model &"-,` + parsedModel.Title + `"Bold&"-,Regular"Risks Summary+000A&D`, }) - if err != nil { - return fmt.Errorf("unable to set header/footer: %w", err) - } - - err = setCellValue(excel, sheetName, []setCellValueCommand{ - {"A1", "Severity"}, - {"B1", "Likelihood"}, - {"C1", "Impact"}, - {"D1", "STRIDE"}, - {"E1", "Function"}, - {"F1", "CWE"}, - {"G1", "Risk category"}, - {"H1", "Technical Asset"}, - {"I1", "Communication Link"}, - {"J1", "RAA %"}, - {"K1", "Identified Risk"}, - {"L1", "Action"}, - {"M1", "Mitigation"}, - {"N1", "Check"}, - {"O1", "ID"}, - {"P1", "Status"}, - {"Q1", "Justification"}, - {"R1", "Date"}, - {"S1", "Checked by"}, - {"T1", "Ticket"}, - }) - if err != nil { - return fmt.Errorf("unable to set cell value: %w", err) - } - - err = setColumnWidth(excel, sheetName, []setColumnWidthCommand{ - {"A", 12}, - {"B", 15}, - {"C", 15}, - {"D", 22}, - {"E", 16}, - {"F", 12}, - {"G", 50}, - {"H", 50}, - {"I", 50}, - {"J", 10}, - {"K", 75}, - {"L", 45}, - {"M", 75}, - {"N", 50}, - {"O", 10}, - {"P", 18}, - {"Q", 75}, - {"R", 18}, - {"S", 20}, - {"T", 20}, - }) - if err != nil { - return fmt.Errorf("unable to set column width: %w", err) + if setHeaderFooterError != nil { + return fmt.Errorf("unable to set header/footer: %w", setHeaderFooterError) } - cellStyles, err := createCellStyles(excel) - if err != nil { - return fmt.Errorf("unable to create cell styles: %w", err) + // set header row + for columnLetter, column := range columns { + setCellValueError := excel.SetCellValue(sheetName, columnLetter+"1", column.Title) + if setCellValueError != nil { + return fmt.Errorf("unable to set cell value: %w", setCellValueError) + } } - excelRow++ // as we have a header line + cellStyles, createCellStylesError := new(ExcelStyles).Init(excel) + if createCellStylesError != nil { + return fmt.Errorf("unable to create cell styles: %w", createCellStylesError) + } + + // get sorted risks + riskItems := make([]RiskItem, 0) for _, category := range types.SortedRiskCategories(parsedModel) { risks := types.SortedRisksOfCategory(parsedModel, category) for _, risk := range risks { - excelRow++ techAsset := parsedModel.TechnicalAssets[risk.MostRelevantTechnicalAssetId] commLink := parsedModel.CommunicationLinks[risk.MostRelevantCommunicationLinkId] - riskTrackingStatus := risk.GetRiskTrackingStatusDefaultingUnchecked(parsedModel) - // content - err := setCellValue(excel, sheetName, []setCellValueCommand{ - {"A" + strconv.Itoa(excelRow), risk.Severity.Title()}, - {"B" + strconv.Itoa(excelRow), risk.ExploitationLikelihood.Title()}, - {"C" + strconv.Itoa(excelRow), risk.ExploitationImpact.Title()}, - {"D" + strconv.Itoa(excelRow), category.STRIDE.Title()}, - {"E" + strconv.Itoa(excelRow), category.Function.Title()}, - {"F" + strconv.Itoa(excelRow), "CWE-" + strconv.Itoa(category.CWE)}, - {"G" + strconv.Itoa(excelRow), category.Title}, - {"H" + strconv.Itoa(excelRow), techAsset.Title}, - {"I" + strconv.Itoa(excelRow), commLink.Title}, - {"K" + strconv.Itoa(excelRow), removeFormattingTags(risk.Title)}, - {"L" + strconv.Itoa(excelRow), category.Action}, - {"M" + strconv.Itoa(excelRow), category.Mitigation}, - {"N" + strconv.Itoa(excelRow), category.Check}, - {"O" + strconv.Itoa(excelRow), risk.SyntheticId}, - {"P" + strconv.Itoa(excelRow), riskTrackingStatus.Title()}, - }) - if err != nil { - return err - } - err = excel.SetCellFloat(sheetName, "J"+strconv.Itoa(excelRow), techAsset.RAA, 0, 32) - if err != nil { - return fmt.Errorf("unable to set cell float: %w", err) - } + date := "" riskTracking := risk.GetRiskTracking(parsedModel) - err = excel.SetCellValue(sheetName, "Q"+strconv.Itoa(excelRow), riskTracking.Justification) - if err != nil { - return fmt.Errorf("unable to set cell value: %w", err) - } if !riskTracking.Date.IsZero() { - err = excel.SetCellValue(sheetName, "R"+strconv.Itoa(excelRow), riskTracking.Date.Format("2006-01-02")) - if err != nil { - return fmt.Errorf("unable to set cell value: %w", err) - } - } - err = excel.SetCellValue(sheetName, "S"+strconv.Itoa(excelRow), riskTracking.CheckedBy) - if err != nil { - return fmt.Errorf("unable to set cell value: %w", err) - } - err = excel.SetCellValue(sheetName, "T"+strconv.Itoa(excelRow), riskTracking.Ticket) - if err != nil { - return fmt.Errorf("unable to set cell value: %w", err) + date = riskTracking.Date.Format("2006-01-02") } - // styles - leftCellsStyle, rightCellStyles := fromSeverityToExcelStyle(riskTrackingStatus, risk.Severity, cellStyles) - err = setCellStyle(excel, sheetName, []setCellStyleCommand{ - {"A" + strconv.Itoa(excelRow), "F" + strconv.Itoa(excelRow), leftCellsStyle}, - {"G" + strconv.Itoa(excelRow), "I" + strconv.Itoa(excelRow), rightCellStyles}, - {"J" + strconv.Itoa(excelRow), "J" + strconv.Itoa(excelRow), cellStyles.blackRight}, - {"K" + strconv.Itoa(excelRow), "K" + strconv.Itoa(excelRow), cellStyles.blackSmall}, - {"L" + strconv.Itoa(excelRow), "L" + strconv.Itoa(excelRow), cellStyles.mitigation}, - {"M" + strconv.Itoa(excelRow), "M" + strconv.Itoa(excelRow), cellStyles.mitigation}, - {"N" + strconv.Itoa(excelRow), "N" + strconv.Itoa(excelRow), cellStyles.mitigation}, - {"O" + strconv.Itoa(excelRow), "O" + strconv.Itoa(excelRow), cellStyles.graySmall}, - {"P" + strconv.Itoa(excelRow), "P" + strconv.Itoa(excelRow), fromRiskTrackingToExcelStyle(riskTrackingStatus, cellStyles)}, - {"Q" + strconv.Itoa(excelRow), "Q" + strconv.Itoa(excelRow), cellStyles.blackSmall}, - {"R" + strconv.Itoa(excelRow), "R" + strconv.Itoa(excelRow), cellStyles.blackCenter}, - {"S" + strconv.Itoa(excelRow), "S" + strconv.Itoa(excelRow), cellStyles.blackCenter}, - {"T" + strconv.Itoa(excelRow), "T" + strconv.Itoa(excelRow), cellStyles.blackLeft}, + riskTrackingStatus := risk.GetRiskTrackingStatusDefaultingUnchecked(parsedModel) + + riskItems = append(riskItems, RiskItem{ + Columns: []string{ + risk.Severity.Title(), + risk.ExploitationLikelihood.Title(), + risk.ExploitationImpact.Title(), + category.STRIDE.Title(), + category.Function.Title(), + "CWE-" + strconv.Itoa(category.CWE), + category.Title, + techAsset.Title, + commLink.Title, + decimal.NewFromFloat(techAsset.RAA).StringFixed(0), + removeFormattingTags(risk.Title), + category.Action, + category.Mitigation, + category.Check, + risk.SyntheticId, + riskTrackingStatus.Title(), + riskTracking.Justification, + date, + riskTracking.CheckedBy, + riskTracking.Ticket, + }, + Status: riskTrackingStatus, + Severity: risk.Severity, }) - if err != nil { - return fmt.Errorf("unable to set cell style: %w", err) - } } } - err = excel.SetCellStyle(sheetName, "A1", "T1", cellStyles.headCenterBoldItalic) - if err != nil { - return fmt.Errorf("unable to set cell style: %w", err) - } - - excel.SetActiveSheet(sheetIndex) - err = excel.SaveAs(filename) - if err != nil { - return fmt.Errorf("unable to save excel file: %w", err) + // group risks + groupedRisk, groupedRiskError := new(RiskGroup).Make(riskItems, columns, config.GroupByColumns) + if groupedRiskError != nil { + return fmt.Errorf("failed to group risks: %w", groupedRiskError) } - return nil -} -type cellStyles struct { - severityCriticalBold int - severityCriticalCenter int - severityHighBold int - severityHighCenter int - severityElevatedBold int - severityElevatedCenter int - severityMediumBold int - severityMediumCenter int - severityLowBold int - severityLowCenter int - redCenter int - greenCenter int - blueCenter int - yellowCenter int - orangeCenter int - grayCenter int - blackLeft int - blackLeftBold int - blackCenter int - blackRight int - blackSmall int - graySmall int - blackBold int - mitigation int - headCenter int - headCenterBoldItalic int - headCenterBold int -} + _ = groupedRisk -func createCellStyles(excel *excelize.File) (*cellStyles, error) { - styleSeverityCriticalBold, err := excel.NewStyle(&excelize.Style{ - Font: &excelize.Font{ - Color: rgbHexColorCriticalRisk(), - Size: 12, - Bold: true, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleSeverityCriticalCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorCriticalRisk(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleSeverityHighBold, err := excel.NewStyle(&excelize.Style{ - Font: &excelize.Font{ - Color: rgbHexColorHighRisk(), - Size: 12, - Bold: true, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleSeverityHighCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorHighRisk(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleSeverityElevatedBold, err := excel.NewStyle(&excelize.Style{ - Font: &excelize.Font{ - Color: rgbHexColorElevatedRisk(), - Size: 12, - Bold: true, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleSeverityElevatedCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorElevatedRisk(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleSeverityMediumBold, err := excel.NewStyle(&excelize.Style{ - Font: &excelize.Font{ - Color: rgbHexColorMediumRisk(), - Size: 12, - Bold: true, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleSeverityMediumCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorMediumRisk(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleSeverityLowBold, err := excel.NewStyle(&excelize.Style{ - Font: &excelize.Font{ - Color: rgbHexColorLowRisk(), - Size: 12, - Bold: true, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleSeverityLowCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorLowRisk(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleRedCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorLowRisk(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleGreenCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorRiskStatusMitigated(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleBlueCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorRiskStatusInProgress(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleYellowCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorRiskStatusAccepted(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleOrangeCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorRiskStatusInDiscussion(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleGrayCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: rgbHexColorRiskStatusFalsePositive(), - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleBlackLeft, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "left", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: "#000000", - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleBlackCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: "#000000", - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleBlackRight, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "right", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: "#000000", - Size: 12, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleBlackSmall, err := excel.NewStyle(&excelize.Style{ - Font: &excelize.Font{ - Color: "#000000", - Size: 10, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) + // write data + writeError := groupedRisk.Write(excel, sheetName, cellStyles) + if writeError != nil { + return fmt.Errorf("failed to write data: %w", writeError) } - styleGraySmall, err := excel.NewStyle(&excelize.Style{ - Font: &excelize.Font{ - Color: rgbHexColorOutOfScope(), - Size: 10, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleBlackBold, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "right", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: "#000000", - Size: 12, - Bold: true, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleBlackLeftBold, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "left", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: "#000000", - Size: 12, - Bold: true, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) + + // set header style + setCellStyleError := excel.SetCellStyle(sheetName, "A1", "T1", cellStyles.headCenterBoldItalic) + if setCellStyleError != nil { + return fmt.Errorf("unable to set cell style: %w", setCellStyleError) } - styleMitigation, err := excel.NewStyle(&excelize.Style{ - Font: &excelize.Font{ - Color: rgbHexColorRiskStatusMitigated(), - Size: 10, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleHeadCenterBoldItalic, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: "#000000", - Bold: true, - Italic: false, - Size: 14, - }, - Fill: excelize.Fill{ - Type: "pattern", - Color: []string{"#eeeeee"}, - Pattern: 1, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - styleHeadCenterBold, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: "#000000", - Size: 14, - Bold: true, - }, - Fill: excelize.Fill{ - Type: "pattern", - Color: []string{"#eeeeee"}, - Pattern: 1, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to create style: %w", err) - } - - styleHeadCenter, err := excel.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "center", - ShrinkToFit: true, - WrapText: false, - }, - Font: &excelize.Font{ - Color: "#000000", - Size: 14, - }, - Fill: excelize.Fill{ - Type: "pattern", - Color: []string{"#eeeeee"}, - Pattern: 1, - }, - }) - if err != nil { - return nil, fmt.Errorf("unable to set cell style: %w", err) - } - - return &cellStyles{ - headCenter: styleHeadCenter, - headCenterBoldItalic: styleHeadCenterBoldItalic, - headCenterBold: styleHeadCenterBold, - severityCriticalBold: styleSeverityCriticalBold, - severityCriticalCenter: styleSeverityCriticalCenter, - severityHighBold: styleSeverityHighBold, - severityHighCenter: styleSeverityHighCenter, - severityElevatedBold: styleSeverityElevatedBold, - severityElevatedCenter: styleSeverityElevatedCenter, - severityMediumBold: styleSeverityMediumBold, - severityMediumCenter: styleSeverityMediumCenter, - severityLowBold: styleSeverityLowBold, - severityLowCenter: styleSeverityLowCenter, - redCenter: styleRedCenter, - greenCenter: styleGreenCenter, - blueCenter: styleBlueCenter, - yellowCenter: styleYellowCenter, - orangeCenter: styleOrangeCenter, - grayCenter: styleGrayCenter, - blackLeft: styleBlackLeft, - blackLeftBold: styleBlackLeftBold, - blackCenter: styleBlackCenter, - blackRight: styleBlackRight, - blackSmall: styleBlackSmall, - graySmall: styleGraySmall, - blackBold: styleBlackBold, - mitigation: styleMitigation, - }, nil -} -func fromRiskTrackingToExcelStyle(riskTrackingStatus types.RiskStatus, cellStyles *cellStyles) int { - switch riskTrackingStatus { - case types.Unchecked: - return cellStyles.redCenter - case types.Mitigated: - return cellStyles.greenCenter - case types.InProgress: - return cellStyles.blueCenter - case types.Accepted: - return cellStyles.yellowCenter - case types.InDiscussion: - return cellStyles.orangeCenter - case types.FalsePositive: - return cellStyles.grayCenter - default: - return cellStyles.blackCenter + // fix column width + cols, colsError := excel.GetCols(sheetName) + if colsError == nil { + for colIndex, col := range cols { + name, columnNumberToNameError := excelize.ColumnNumberToName(colIndex + 1) + if columnNumberToNameError != nil { + return columnNumberToNameError + } + + var largestWidth float64 = 0 + for rowIndex, rowCell := range col { + cellWidth := float64(utf8.RuneCountInString(rowCell) + 1) // + 1 for margin + + cellName, coordinateError := excelize.CoordinatesToCellName(colIndex+1, rowIndex+1) + if coordinateError == nil { + style, styleError := excel.GetCellStyle(sheetName, cellName) + if styleError == nil { + styleDetails, detailsError := excel.GetStyle(style) + if detailsError == nil { + if styleDetails.Font != nil && styleDetails.Font.Size > 0 { + cellWidth *= styleDetails.Font.Size / 14. + } + } + } + } + + if cellWidth > largestWidth { + largestWidth = cellWidth + } + } + + var minWidth float64 + for columnLetter := range columns { + if strings.EqualFold(columnLetter, name) { + minWidth = columns[columnLetter].Width + } + } + + if largestWidth < 100 { + minWidth = largestWidth + } + + if minWidth < 8 { + minWidth = 8 + } + + setColWidthError := excel.SetColWidth(sheetName, name, name, minWidth) + if setColWidthError != nil { + return setColWidthError + } + } } -} -func fromSeverityToExcelStyle(riskTrackingStatus types.RiskStatus, severity types.RiskSeverity, cellStyles *cellStyles) (int, int) { - - if riskTrackingStatus.IsStillAtRisk() { - switch severity { - case types.CriticalSeverity: - return cellStyles.severityCriticalCenter, cellStyles.severityCriticalBold - case types.HighSeverity: - return cellStyles.severityHighCenter, cellStyles.severityHighBold - case types.ElevatedSeverity: - return cellStyles.severityElevatedCenter, cellStyles.severityElevatedBold - case types.MediumSeverity: - return cellStyles.severityMediumCenter, cellStyles.severityMediumBold - case types.LowSeverity: - return cellStyles.severityLowCenter, cellStyles.severityLowBold + // hide some columns + for columnLetter, column := range columns { + for _, hiddenColumn := range config.HideColumns { + if strings.EqualFold(hiddenColumn, column.Title) { + hideColumnError := excel.SetColVisible(sheetName, columnLetter, false) + if hideColumnError != nil { + return fmt.Errorf("unable to hide column: %w", hideColumnError) + } + } } } - return cellStyles.blackCenter, cellStyles.blackBold -} -type setCellStyleCommand struct { - hCell string - vCell string - Style int -} + // freeze header + freezeError := excel.SetPanes(sheetName, &excelize.Panes{ + Freeze: true, + Split: false, + XSplit: 0, + YSplit: 1, + TopLeftCell: "A2", + ActivePane: "bottomLeft", + }) + if freezeError != nil { + return fmt.Errorf("unable to freeze header: %w", freezeError) + } -func setCellStyle(excel *excelize.File, sheetName string, commands []setCellStyleCommand) error { - for _, command := range commands { - err := excel.SetCellStyle(sheetName, command.hCell, command.vCell, command.Style) - if err != nil { - return fmt.Errorf("unable to set cell style: %w", err) - } + excel.SetActiveSheet(sheetIndex) + + // save file + saveAsError := excel.SaveAs(filename) + if saveAsError != nil { + return fmt.Errorf("unable to save excel file: %w", saveAsError) } + return nil } @@ -733,10 +283,13 @@ func WriteTagsExcelToFile(parsedModel *types.ParsedModel, filename string) error sortedTagsAvailable := parsedModel.TagsActuallyUsed() sort.Strings(sortedTagsAvailable) - axis := "" for i, tag := range sortedTagsAvailable { - axis = determineColumnLetter(i) - err = excel.SetCellValue(sheetName, axis+"1", tag) + cellName, coordinatesToCellNameError := excelize.CoordinatesToCellName(i+2, 1) + if coordinatesToCellNameError != nil { + return fmt.Errorf("failed to get cell coordinates from [%d, %d]: %w", i+2, 1, coordinatesToCellNameError) + } + + err = excel.SetCellValue(sheetName, cellName, tag) if err != nil { return err } @@ -747,46 +300,47 @@ func WriteTagsExcelToFile(parsedModel *types.ParsedModel, filename string) error return err } + lastColumn, _ := excelize.ColumnNumberToName(len(sortedTagsAvailable) + 2) if len(sortedTagsAvailable) > 0 { - err = excel.SetColWidth(sheetName, "B", axis, 35) + err = excel.SetColWidth(sheetName, "B", lastColumn, 35) } if err != nil { return err } - cellStyles, err := createCellStyles(excel) - if err != nil { - return err + cellStyles, createCellStylesError := new(ExcelStyles).Init(excel) + if createCellStylesError != nil { + return fmt.Errorf("unable to create cell styles: %w", createCellStylesError) } excelRow++ // as we have a header line if len(sortedTagsAvailable) > 0 { for _, techAsset := range sortedTechnicalAssetsByTitle(parsedModel) { - err := writeRow(excel, &excelRow, sheetName, axis, cellStyles.blackLeftBold, cellStyles.blackCenter, sortedTagsAvailable, techAsset.Title, techAsset.Tags) + err := writeRow(excel, &excelRow, sheetName, lastColumn, cellStyles.blackLeftBold, cellStyles.blackCenter, sortedTagsAvailable, techAsset.Title, techAsset.Tags) if err != nil { return fmt.Errorf("unable to write row: %w", err) } for _, commLink := range techAsset.CommunicationLinksSorted() { - err := writeRow(excel, &excelRow, sheetName, axis, cellStyles.blackLeftBold, cellStyles.blackCenter, sortedTagsAvailable, commLink.Title, commLink.Tags) + err := writeRow(excel, &excelRow, sheetName, lastColumn, cellStyles.blackLeftBold, cellStyles.blackCenter, sortedTagsAvailable, commLink.Title, commLink.Tags) if err != nil { return fmt.Errorf("unable to write row: %w", err) } } } for _, dataAsset := range sortedDataAssetsByTitle(parsedModel) { - err := writeRow(excel, &excelRow, sheetName, axis, cellStyles.blackLeftBold, cellStyles.blackCenter, sortedTagsAvailable, dataAsset.Title, dataAsset.Tags) + err := writeRow(excel, &excelRow, sheetName, lastColumn, cellStyles.blackLeftBold, cellStyles.blackCenter, sortedTagsAvailable, dataAsset.Title, dataAsset.Tags) if err != nil { return fmt.Errorf("unable to write row: %w", err) } } for _, trustBoundary := range sortedTrustBoundariesByTitle(parsedModel) { - err := writeRow(excel, &excelRow, sheetName, axis, cellStyles.blackLeftBold, cellStyles.blackCenter, sortedTagsAvailable, trustBoundary.Title, trustBoundary.Tags) + err := writeRow(excel, &excelRow, sheetName, lastColumn, cellStyles.blackLeftBold, cellStyles.blackCenter, sortedTagsAvailable, trustBoundary.Title, trustBoundary.Tags) if err != nil { return fmt.Errorf("unable to write row: %w", err) } } for _, sharedRuntime := range sortedSharedRuntimesByTitle(parsedModel) { - err := writeRow(excel, &excelRow, sheetName, axis, cellStyles.blackLeftBold, cellStyles.blackCenter, sortedTagsAvailable, sharedRuntime.Title, sharedRuntime.Tags) + err := writeRow(excel, &excelRow, sheetName, lastColumn, cellStyles.blackLeftBold, cellStyles.blackCenter, sortedTagsAvailable, sharedRuntime.Title, sharedRuntime.Tags) if err != nil { return fmt.Errorf("unable to write row: %w", err) } @@ -795,7 +349,7 @@ func WriteTagsExcelToFile(parsedModel *types.ParsedModel, filename string) error err = excel.SetCellStyle(sheetName, "A1", "A1", cellStyles.headCenterBold) if len(sortedTagsAvailable) > 0 { - err = excel.SetCellStyle(sheetName, "B1", axis+"1", cellStyles.headCenter) + err = excel.SetCellStyle(sheetName, "B1", lastColumn+"1", cellStyles.headCenter) } if err != nil { return fmt.Errorf("unable to set cell style: %w", err) @@ -830,72 +384,57 @@ func sortedDataAssetsByTitle(parsedModel *types.ParsedModel) []types.DataAsset { func writeRow(excel *excelize.File, excelRow *int, sheetName string, axis string, styleBlackLeftBold int, styleBlackCenter int, sortedTags []string, assetTitle string, tagsUsed []string) error { *excelRow++ - err := excel.SetCellValue(sheetName, "A"+strconv.Itoa(*excelRow), assetTitle) + + firstCellName, firstCoordinatesToCellNameError := excelize.CoordinatesToCellName(1, *excelRow) + if firstCoordinatesToCellNameError != nil { + return fmt.Errorf("failed to get cell coordinates from [%d, %d]: %w", 1, *excelRow, firstCoordinatesToCellNameError) + } + + err := excel.SetCellValue(sheetName, firstCellName, assetTitle) if err != nil { return fmt.Errorf("unable to write row: %w", err) } + for i, tag := range sortedTags { if contains(tagsUsed, tag) { - err = excel.SetCellValue(sheetName, determineColumnLetter(i)+strconv.Itoa(*excelRow), "X") + cellName, coordinatesToCellNameError := excelize.CoordinatesToCellName(i+2, *excelRow) + if coordinatesToCellNameError != nil { + return fmt.Errorf("failed to get cell coordinates from [%d, %d]: %w", i+2, *excelRow, coordinatesToCellNameError) + } + + err = excel.SetCellValue(sheetName, cellName, "X") if err != nil { return fmt.Errorf("unable to write row: %w", err) } } } - err = excel.SetCellStyle(sheetName, "A"+strconv.Itoa(*excelRow), "A"+strconv.Itoa(*excelRow), styleBlackLeftBold) + + err = excel.SetCellStyle(sheetName, firstCellName, firstCellName, styleBlackLeftBold) if err != nil { return fmt.Errorf("unable to write row: %w", err) } - err = excel.SetCellStyle(sheetName, "B"+strconv.Itoa(*excelRow), axis+strconv.Itoa(*excelRow), styleBlackCenter) + + secondCellName, secondCoordinatesToCellNameError := excelize.CoordinatesToCellName(2, *excelRow) + if secondCoordinatesToCellNameError != nil { + return fmt.Errorf("failed to get cell coordinates from [%d, %d]: %w", 2, *excelRow, secondCoordinatesToCellNameError) + } + + lastCellName, lastCoordinatesToCellNameError := excelize.CoordinatesToCellName(len(sortedTags)+2, *excelRow) + if lastCoordinatesToCellNameError != nil { + return fmt.Errorf("failed to get cell coordinates from [%d, %d]: %w", len(sortedTags)+2, *excelRow, lastCoordinatesToCellNameError) + } + + err = excel.SetCellStyle(sheetName, secondCellName, lastCellName, styleBlackCenter) if err != nil { return fmt.Errorf("unable to write row: %w", err) } - return nil -} -func determineColumnLetter(i int) string { - alphabet := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"} - // can only have 700 columns in Excel that way, but that should be more than usable anyway ;)... otherwise think about your model... - i++ - if i < 26 { - return alphabet[i] - } - return alphabet[(i/26)-1] + alphabet[i%26] + return nil } -func removeFormattingTags(content string) interface{} { +func removeFormattingTags(content string) string { result := strings.ReplaceAll(strings.ReplaceAll(content, "", ""), "", "") result = strings.ReplaceAll(strings.ReplaceAll(result, "", ""), "", "") result = strings.ReplaceAll(strings.ReplaceAll(result, "", ""), "", "") return result } - -type setCellValueCommand struct { - cell string - value interface{} -} - -func setCellValue(excel *excelize.File, sheetName string, cmds []setCellValueCommand) error { - for _, cmd := range cmds { - err := excel.SetCellValue(sheetName, cmd.cell, cmd.value) - if err != nil { - return err - } - } - return nil -} - -type setColumnWidthCommand struct { - column string - width float64 -} - -func setColumnWidth(excel *excelize.File, sheetName string, cmds []setColumnWidthCommand) error { - for _, cmd := range cmds { - err := excel.SetColWidth(sheetName, cmd.column, cmd.column, cmd.width) - if err != nil { - return err - } - } - return nil -} diff --git a/pkg/report/generate.go b/pkg/report/generate.go index 53b89633..433833c4 100644 --- a/pkg/report/generate.go +++ b/pkg/report/generate.go @@ -68,7 +68,7 @@ func Generate(config *common.Config, readResult *model.ReadResult, commands *Gen } err = GenerateDataFlowDiagramGraphvizImage(dotFile, config.OutputFolder, - config.TempFolder, config.DataFlowDiagramFilenamePNG, progressReporter) + config.TempFolder, config.DataFlowDiagramFilenamePNG, progressReporter, config.KeepDiagramSourceFiles) if err != nil { progressReporter.Warn(err) } @@ -125,7 +125,7 @@ func Generate(config *common.Config, readResult *model.ReadResult, commands *Gen // risks Excel if commands.RisksExcel { progressReporter.Info("Writing risks excel") - err := WriteRisksExcelToFile(readResult.ParsedModel, filepath.Join(config.OutputFolder, config.ExcelRisksFilename)) + err := WriteRisksExcelToFile(readResult.ParsedModel, filepath.Join(config.OutputFolder, config.ExcelRisksFilename), config) if err != nil { return err } diff --git a/pkg/report/graphviz.go b/pkg/report/graphviz.go index 0912d75b..a9a63532 100644 --- a/pkg/report/graphviz.go +++ b/pkg/report/graphviz.go @@ -394,20 +394,24 @@ func determineArrowColor(cl types.CommunicationLink, parsedModel *types.ParsedMo } func GenerateDataFlowDiagramGraphvizImage(dotFile *os.File, targetDir string, - tempFolder, dataFlowDiagramFilenamePNG string, progressReporter progressReporter) error { + tempFolder, dataFlowDiagramFilenamePNG string, progressReporter progressReporter, keepGraphVizDataFile bool) error { progressReporter.Info("Rendering data flow diagram input") // tmp files tmpFileDOT, err := os.CreateTemp(tempFolder, "diagram-*-.gv") if err != nil { return fmt.Errorf("error creating temp file: %v", err) } - defer func() { _ = os.Remove(tmpFileDOT.Name()) }() + if !keepGraphVizDataFile { + defer func() { _ = os.Remove(tmpFileDOT.Name()) }() + } tmpFilePNG, err := os.CreateTemp(tempFolder, "diagram-*-.png") if err != nil { return fmt.Errorf("error creating temp file: %v", err) } - defer func() { _ = os.Remove(tmpFilePNG.Name()) }() + if !keepGraphVizDataFile { + defer func() { _ = os.Remove(tmpFilePNG.Name()) }() + } // copy into tmp file as input inputDOT, err := os.ReadFile(dotFile.Name()) diff --git a/pkg/report/report.go b/pkg/report/report.go index 99a3d341..17ee85c5 100644 --- a/pkg/report/report.go +++ b/pkg/report/report.go @@ -163,9 +163,9 @@ func (r *pdfReporter) parseBackgroundTemplate(templateFilename string) { checkErr(err) file, err := os.CreateTemp("", "background-*-.r.pdf") checkErr(err) - defer os.Remove(file.Name()) + defer os.Remove(file.Title()) backgroundBytes := imageBox.MustBytes("background.r.pdf") - err = os.WriteFile(file.Name(), backgroundBytes, 0644) + err = os.WriteFile(file.Title(), backgroundBytes, 0644) checkErr(err) */ r.coverTemplateId = gofpdi.ImportPage(r.pdf, templateFilename, 1, "/MediaBox") @@ -4352,15 +4352,15 @@ func (r *pdfReporter) embedDataFlowDiagram(diagramFilenamePNG string, tempFolder // now rotate left by 90 degrees rotatedFile, err := os.CreateTemp(tempFolder, "diagram-*-.png") checkErr(err) - defer os.Remove(rotatedFile.Name()) + defer os.Remove(rotatedFile.Title()) dstImage := image.NewRGBA(image.Rect(0, 0, srcDimensions.Dy(), srcDimensions.Dx())) err = graphics.Rotate(dstImage, srcImage, &graphics.RotateOptions{-1 * math.Pi / 2.0}) checkErr(err) - newImage, _ := os.Create(rotatedFile.Name()) + newImage, _ := os.Create(rotatedFile.Title()) defer newImage.Close() err = png.Encode(newImage, dstImage) checkErr(err) - diagramFilenamePNG = rotatedFile.Name() + diagramFilenamePNG = rotatedFile.Title() } } else { r.pdf.AddPage() @@ -4452,15 +4452,15 @@ func (r *pdfReporter) embedDataRiskMapping(diagramFilenamePNG string, tempFolder // now rotate left by 90 degrees rotatedFile, err := os.CreateTemp(tempFolder, "diagram-*-.png") checkErr(err) - defer os.Remove(rotatedFile.Name()) + defer os.Remove(rotatedFile.Title()) dstImage := image.NewRGBA(image.Rect(0, 0, srcDimensions.Dy(), srcDimensions.Dx())) err = graphics.Rotate(dstImage, srcImage, &graphics.RotateOptions{-1 * math.Pi / 2.0}) checkErr(err) - newImage, _ := os.Create(rotatedFile.Name()) + newImage, _ := os.Create(rotatedFile.Title()) defer newImage.Close() err = png.Encode(newImage, dstImage) checkErr(err) - diagramFilenamePNG = rotatedFile.Name() + diagramFilenamePNG = rotatedFile.Title() } } else { r.pdf.AddPage() diff --git a/pkg/report/risk-group.go b/pkg/report/risk-group.go new file mode 100644 index 00000000..efec002c --- /dev/null +++ b/pkg/report/risk-group.go @@ -0,0 +1,127 @@ +package report + +import ( + "fmt" + "github.com/mpvl/unique" + "github.com/xuri/excelize/v2" + "sort" +) + +type RiskGroup struct { + Groups map[string]*RiskGroup + Items []RiskItem +} + +func (what *RiskGroup) Init(riskItems []RiskItem) *RiskGroup { + *what = RiskGroup{ + Groups: make(map[string]*RiskGroup), + Items: riskItems, + } + + return what +} + +func (what *RiskGroup) SortedGroups() []string { + groups := make([]string, 0) + for group := range what.Groups { + groups = append(groups, group) + } + + sort.Strings(groups) + unique.Strings(&groups) + + return groups +} + +func (what *RiskGroup) Make(riskItems []RiskItem, columns ExcelColumns, groupBy []string) (*RiskGroup, error) { + what.Init(riskItems) + + if len(groupBy) == 0 { + return what, nil + } + + groupName, groupBy := groupBy[0], groupBy[1:] + column := columns.FindColumnIndexByTitle(groupName) + if column < 0 { + return what, fmt.Errorf("unable to find column %q", groupName) + } + + values := what.uniqueValues(column) + for _, value := range values { + subItems := make([]RiskItem, 0) + for _, item := range what.Items { + if item.Columns[column] == value { + subItems = append(subItems, item) + } + } + + group, groupError := new(RiskGroup).Make(subItems, columns, groupBy) + if groupError != nil { + return what, fmt.Errorf("unable to create group: %w", groupError) + } + + what.Groups[value] = group + } + + return what, nil +} + +func (what *RiskGroup) Write(excel *excelize.File, sheetName string, cellStyles *ExcelStyles) error { + if len(what.Groups) == 0 { + _, writeError := what.writeGroup(excel, sheetName, cellStyles, 0) + return writeError + } + + var writeError error + excelRow := 0 + for _, group := range what.SortedGroups() { + excelRow, writeError = what.Groups[group].writeGroup(excel, sheetName, cellStyles, excelRow) + if writeError != nil { + return writeError + } + } + + return writeError +} + +func (what *RiskGroup) writeGroup(excel *excelize.File, sheetName string, cellStyles *ExcelStyles, excelRow int) (int, error) { + for _, risk := range what.Items { + excelRow++ + + for columnIndex, column := range risk.Columns { + cellName, coordinatesToCellNameError := excelize.CoordinatesToCellName(columnIndex+1, excelRow+1) + if coordinatesToCellNameError != nil { + return excelRow, fmt.Errorf("failed to get cell coordinates from [%d, %d]: %w", columnIndex+1, excelRow+1, coordinatesToCellNameError) + } + + setCellValueError := excel.SetCellValue(sheetName, cellName, column) + if setCellValueError != nil { + return excelRow, setCellValueError + } + + columnName, columnNameError := excelize.ColumnNumberToName(columnIndex + 1) + if columnNameError != nil { + return excelRow, fmt.Errorf("failed to get cell coordinates from column [%d]: %w", columnIndex+1, columnNameError) + } + + setCellStyleError := excel.SetCellStyle(sheetName, cellName, cellName, cellStyles.Get(columnName, risk.Status, risk.Severity)) + if setCellStyleError != nil { + return excelRow, fmt.Errorf("failed to set cell style: %w", setCellStyleError) + } + } + } + + return excelRow, nil +} + +func (what *RiskGroup) uniqueValues(column int) []string { + values := make([]string, 0) + for _, risk := range what.Items { + values = append(values, risk.Columns[column]) + } + + sort.Strings(values) + unique.Strings(&values) + + return values +} diff --git a/pkg/report/risk-item.go b/pkg/report/risk-item.go new file mode 100644 index 00000000..5a12efbb --- /dev/null +++ b/pkg/report/risk-item.go @@ -0,0 +1,9 @@ +package report + +import "github.com/threagile/threagile/pkg/security/types" + +type RiskItem struct { + Columns []string + Status types.RiskStatus + Severity types.RiskSeverity +} diff --git a/pkg/security/types/risk_status.go b/pkg/security/types/risk_status.go index 3da71f0a..bdddd221 100644 --- a/pkg/security/types/risk_status.go +++ b/pkg/security/types/risk_status.go @@ -62,7 +62,7 @@ func (what RiskStatus) Explain() string { } func (what RiskStatus) Title() string { - return [...]string{"Unchecked", "in Discussion", "Accepted", "in Progress", "Mitigated", "False Positive"}[what] + return [...]string{"Unchecked", "In Discussion", "Accepted", "In Progress", "Mitigated", "False Positive"}[what] } func (what RiskStatus) IsStillAtRisk() bool { diff --git a/pkg/server/zip.go b/pkg/server/zip.go index 431d4cc9..78d7b731 100644 --- a/pkg/server/zip.go +++ b/pkg/server/zip.go @@ -111,7 +111,7 @@ func addFileToZip(zipWriter *zip.Writer, filename string) error { // Using FileInfoHeader() above only uses the basename of the file. If we want // to preserve the folder structure we can overwrite this with the full path. - //header.Name = filename + //header.Title = filename // Change to deflate to gain better compression // see http://golang.org/pkg/archive/zip/#pkg-constants