From 52ed5f39ff7b7fc7ad21b3a6a16701388a95b4cd Mon Sep 17 00:00:00 2001 From: Yevhen Zavhorodnii Date: Tue, 26 Nov 2024 13:29:43 +0000 Subject: [PATCH] Allow to add legend to diagram --- docs/config.md | 3 +- internal/threagile/config.go | 10 ++ pkg/model/read.go | 1 + pkg/report/generate.go | 3 +- pkg/report/graphviz.go | 209 ++++++++++++++++------------------- pkg/server/server.go | 1 + 6 files changed, 109 insertions(+), 118 deletions(-) diff --git a/docs/config.md b/docs/config.md index be628919..e1839462 100644 --- a/docs/config.md +++ b/docs/config.md @@ -45,7 +45,8 @@ This config keys is used when application run in [analyze mode](./mode-analyze.m | `DiagramDPI` | int | The same as `-diagram-dpi` [flags](./flags.md) | see [flags](./flags.md) | | `GraphvizDPI` | TBD | The same as `-verbose` or `--v` at [flags](./flags.md) | see [flags](./flags.md) | | `MaxGraphvizDPI` | TBD | The same as `-verbose` or `--v` at [flags](./flags.md) | see [flags](./flags.md) | -| `AddModelTitle` | TBD | Identify if model title shall be added to diagram | false | +| `AddModelTitle` | TBD | Identify if model title shall be added to diagram | false | +| `AddLegend` | TBD | Identify if legend shall be added to diagram | false | ### Excel config keys diff --git a/internal/threagile/config.go b/internal/threagile/config.go index 9326fa2f..c3d737b9 100644 --- a/internal/threagile/config.go +++ b/internal/threagile/config.go @@ -60,6 +60,7 @@ type Config struct { BackupHistoryFilesToKeepValue int `json:"BackupHistoryFilesToKeep,omitempty" yaml:"BackupHistoryFilesToKeep"` AddModelTitleValue bool `json:"AddModelTitle,omitempty" yaml:"AddModelTitle"` + AddLegendValue bool `json:"AddLegend,omitempty" yaml:"AddLegend"` KeepDiagramSourceFilesValue bool `json:"KeepDiagramSourceFiles,omitempty" yaml:"KeepDiagramSourceFiles"` IgnoreOrphanedRiskTrackingValue bool `json:"IgnoreOrphanedRiskTracking,omitempty" yaml:"IgnoreOrphanedRiskTracking"` @@ -120,6 +121,7 @@ type ConfigGetter interface { GetMaxGraphvizDPI() int GetBackupHistoryFilesToKeep() int GetAddModelTitle() bool + GetAddLegend() bool GetKeepDiagramSourceFiles() bool GetIgnoreOrphanedRiskTracking() bool GetSkipDataFlowDiagram() bool @@ -204,6 +206,7 @@ func (c *Config) Defaults(buildTimestamp string) *Config { BackupHistoryFilesToKeepValue: DefaultBackupHistoryFilesToKeep, AddModelTitleValue: false, + AddLegendValue: false, KeepDiagramSourceFilesValue: false, IgnoreOrphanedRiskTrackingValue: false, @@ -474,6 +477,9 @@ func (c *Config) Merge(config Config, values map[string]any) { case strings.ToLower("AddModelTitle"): c.AddModelTitleValue = config.AddModelTitleValue + case strings.ToLower("AddLegend"): + c.AddLegendValue = config.AddLegendValue + case strings.ToLower("KeepDiagramSourceFiles"): c.KeepDiagramSourceFilesValue = config.KeepDiagramSourceFilesValue @@ -772,6 +778,10 @@ func (c *Config) GetAddModelTitle() bool { return c.AddModelTitleValue } +func (c *Config) GetAddLegend() bool { + return c.AddLegendValue +} + func (c *Config) GetKeepDiagramSourceFiles() bool { return c.KeepDiagramSourceFilesValue } diff --git a/pkg/model/read.go b/pkg/model/read.go index b4faa664..8f835ef5 100644 --- a/pkg/model/read.go +++ b/pkg/model/read.go @@ -65,6 +65,7 @@ type configReader interface { GetMaxGraphvizDPI() int GetBackupHistoryFilesToKeep() int GetAddModelTitle() bool + GetAddLegend() bool GetKeepDiagramSourceFiles() bool GetIgnoreOrphanedRiskTracking() bool GetThreagileVersion() string diff --git a/pkg/report/generate.go b/pkg/report/generate.go index 567951dc..0235091b 100644 --- a/pkg/report/generate.go +++ b/pkg/report/generate.go @@ -76,6 +76,7 @@ type reportConfigReader interface { GetKeepDiagramSourceFiles() bool GetAddModelTitle() bool + GetAddLegend() bool GetReportConfigurationHideChapters() map[ChaptersToShowHide]bool } @@ -117,7 +118,7 @@ func Generate(config reportConfigReader, readResult *model.ReadResult, commands gvFile = tmpFileGV.Name() defer func() { _ = os.Remove(gvFile) }() } - dotFile, err := WriteDataFlowDiagramGraphvizDOT(readResult.ParsedModel, gvFile, diagramDPI, config.GetAddModelTitle(), progressReporter) + dotFile, err := WriteDataFlowDiagramGraphvizDOT(readResult.ParsedModel, gvFile, diagramDPI, config.GetAddModelTitle(), config.GetAddLegend(), progressReporter) if err != nil { return fmt.Errorf("error while generating data flow diagram: %w", err) } diff --git a/pkg/report/graphviz.go b/pkg/report/graphviz.go index 09452b7c..1a1f1e2d 100644 --- a/pkg/report/graphviz.go +++ b/pkg/report/graphviz.go @@ -15,13 +15,61 @@ import ( ) func WriteDataFlowDiagramGraphvizDOT(parsedModel *types.Model, - diagramFilenameDOT string, dpi int, addModelTitle bool, + diagramFilenameDOT string, dpi int, addModelTitle bool, addLegend bool, progressReporter progressReporter) (*os.File, error) { progressReporter.Info("Writing data flow diagram input") var dotContent strings.Builder dotContent.WriteString("digraph generatedModel { concentrate=false \n") + if addLegend { + dotContent.WriteString("subgraph cluster_shape_legend { \n") + dotContent.WriteString(" label=\"Shape legend\"; \n") + dotContent.WriteString(" color=\"lightgrey\"; \n") + dotContent.WriteString(" style=\"dashed\"; \n") + dotContent.WriteString(makeLegendNode("external_entity_item", "External Entity", "0", "black", "box", "solid", "filled", "2.0", VeryLightGray, "1", Black)) + dotContent.WriteString(makeLegendNode("process_item", "Process", "0", "black", "ellipse", "solid", "filled", "2.0", VeryLightGray, "1", Black)) + dotContent.WriteString(makeLegendNode("datastore_item", "Datastore", "0", "black", "cylinder", "solid", "filled", "2.0", VeryLightGray, "1", Black)) + dotContent.WriteString(makeLegendNode("used_as_client_item", "Used as client", "0", "black", "octagon", "solid", "filled", "2.0", VeryLightGray, "1", Black)) + dotContent.WriteString("} \n") + + dotContent.WriteString("subgraph cluster_tenant_legend { \n") + dotContent.WriteString(" label=\"Tenant legend\"; \n") + dotContent.WriteString(" color=\"lightgrey\"; \n") + dotContent.WriteString(" style=\"dashed\"; \n") + dotContent.WriteString(makeLegendNode("single_tenant", "Single tenant", "0", "black", "box", "solid", "filled", "2.0", VeryLightGray, "1", Black)) + dotContent.WriteString(makeLegendNode("multi_tenant", "Multitenant", "1", "black", "box", "solid", "filled", "2.0", VeryLightGray, "1", Black)) + dotContent.WriteString("} \n") + + dotContent.WriteString("subgraph cluster_label_legend { \n") + dotContent.WriteString(" label=\"Label color legend\"; \n") + dotContent.WriteString(" color=\"lightgrey\"; \n") + dotContent.WriteString(" style=\"dashed\"; \n") + dotContent.WriteString(makeLegendNode("mission_critical", "Mission Critical Asset", "0", Red, "box", "solid", "filled", "3.0", VeryLightGray, "1", Red)) + dotContent.WriteString(makeLegendNode("critical", "Critical Asset", "0", Amber, "box", "solid", "filled", "3.0", VeryLightGray, "1", Amber)) + dotContent.WriteString(makeLegendNode("other", "Important and Other Assets", "0", Black, "box", "solid", "filled", "2.0", VeryLightGray, "1", Black)) + dotContent.WriteString("} \n") + + dotContent.WriteString("subgraph cluster_border_line_legend { \n") + dotContent.WriteString(" label=\"Label color legend\"; \n") + dotContent.WriteString(" color=\"lightgrey\"; \n") + dotContent.WriteString(" style=\"dashed\"; \n") + dotContent.WriteString(makeLegendNode("dotted", "Model forgery attempt", "0", Black, "box", "dotted ", "filled", "2.0", VeryLightGray, "1", Black)) + dotContent.WriteString(makeLegendNode("solid", "Normal", "0", Black, "box", "solid", "filled", "2.0", VeryLightGray, "1", Black)) + dotContent.WriteString("} \n") + + dotContent.WriteString("subgraph cluster_fill_legend { \n") + dotContent.WriteString(" label=\"Shape fill legend (darker for physical machines, brighter for container and even more brighter for serverless\"; \n") + dotContent.WriteString(" color=\"lightgrey\"; \n") + dotContent.WriteString(" style=\"dashed\"; \n") + dotContent.WriteString(makeLegendNode("invalid_item", "No data processed or stored, or using unknown technology, or no communication links", "0", "black", "box", "solid", "filled", "2.0", LightPink, "1", Black)) + dotContent.WriteString(makeLegendNode("internet", "Asset used over the internet", "0", "black", "box", "solid", "filled", "2.0", ExtremeLightBlue, "1", Black)) + dotContent.WriteString(makeLegendNode("out_of_scope", "Out of scope", "0", "black", "box", "solid", "filled", "2.0", OutOfScopeFancy, "1", Black)) + dotContent.WriteString(makeLegendNode("custom_developed_part", "Custom developed part", "0", "black", "box", "solid", "filled", "2.0", CustomDevelopedParts, "1", Black)) + dotContent.WriteString(makeLegendNode("other_assets", "Other assets", "0", "black", "box", "solid", "filled", "2.0", VeryLightGray, "1", Black)) + dotContent.WriteString("} \n") + } + // Metadata init =============================================================================== tweaks := "" if parsedModel.DiagramTweakNodesep > 0 { @@ -252,11 +300,8 @@ func WriteDataFlowDiagramGraphvizDOT(parsedModel *types.Model, return nil, fmt.Errorf("error while making diagram same-rank node tweaks: %w", err) } dotContent.WriteString(diagramSameRankNodeTweaks) - dotContent.WriteString("}") - //fmt.Println(dotContent.String()) - // Write the DOT file file, err := os.Create(filepath.Clean(diagramFilenameDOT)) if err != nil { @@ -270,6 +315,14 @@ func WriteDataFlowDiagramGraphvizDOT(parsedModel *types.Model, return file, nil } +func makeLegendNode(id, title, compartmentBorder, labelColor, shape, borderLineStyle, shapeStyle, borderPenWidth, shapeFillColor, shapePeripheries, shapeBorderColor string) string { + return " " + id + ` [ +label=<
list of technologies` + `
technical asset size
` + encode(title) + `
attacker attractiveness level
> +shape=` + shape + ` style="` + borderLineStyle + `,` + shapeStyle + `" penwidth="` + borderPenWidth + `" fillcolor="` + shapeFillColor + `" +peripheries=` + shapePeripheries + ` +color="` + shapeBorderColor + "\"\n ]; " +} + // Pen Widths: func determineArrowPenWidth(cl *types.CommunicationLink, parsedModel *types.Model) string { @@ -362,35 +415,6 @@ func determineArrowColor(cl *types.CommunicationLink, parsedModel *types.Model) } // default return Black - /* - } else if dataFlow.Authentication != NoneAuthentication { - return Black - } else { - // check for red - for _, sentDataAsset := range dataFlow.DataAssetsSent { // first check if any red? - if ParsedModelRoot.DataAssets[sentDataAsset].Integrity == MissionCritical { - return Red - } - } - for _, receivedDataAsset := range dataFlow.DataAssetsReceived { // first check if any red? - if ParsedModelRoot.DataAssets[receivedDataAsset].Integrity == MissionCritical { - return Red - } - } - // check for amber - for _, sentDataAsset := range dataFlow.DataAssetsSent { // then check if any amber? - if ParsedModelRoot.DataAssets[sentDataAsset].Integrity == Critical { - return Amber - } - } - for _, receivedDataAsset := range dataFlow.DataAssetsReceived { // then check if any amber? - if ParsedModelRoot.DataAssets[receivedDataAsset].Integrity == Critical { - return Amber - } - } - return Black - } - */ } func GenerateDataFlowDiagramGraphvizImage(dotFile *os.File, targetDir string, @@ -638,48 +662,48 @@ func makeTechAssetNode(parsedModel *types.Model, technicalAsset *types.Technical return " " + hash(technicalAsset.Id) + ` [ shape="box" style="filled" fillcolor="` + color + `" label=<` + encode(technicalAsset.Title) + `> penwidth="3.0" color="` + color + `" ]; ` - } else { - var shape, title string - var lineBreak = "" - switch technicalAsset.Type { - case types.ExternalEntity: - shape = "box" - title = technicalAsset.Title - case types.Process: - shape = "ellipse" - title = technicalAsset.Title - case types.Datastore: - shape = "cylinder" - title = technicalAsset.Title - if technicalAsset.Redundant { - lineBreak = "
" - } - } + } - if technicalAsset.UsedAsClientByHuman { - shape = "octagon" + var shape, title string + var lineBreak = "" + switch technicalAsset.Type { + case types.ExternalEntity: + shape = "box" + title = technicalAsset.Title + case types.Process: + shape = "ellipse" + title = technicalAsset.Title + case types.Datastore: + shape = "cylinder" + title = technicalAsset.Title + if technicalAsset.Redundant { + lineBreak = "
" } + } - // RAA = Relative Attacker Attractiveness - raa := technicalAsset.RAA - var attackerAttractivenessLabel string - if technicalAsset.OutOfScope { - attackerAttractivenessLabel = "RAA: out of scope" - } else { - attackerAttractivenessLabel = "RAA: " + fmt.Sprintf("%.0f", raa) + " %" - } + if technicalAsset.UsedAsClientByHuman { + shape = "octagon" + } - compartmentBorder := "0" - if technicalAsset.MultiTenant { - compartmentBorder = "1" - } + // RAA = Relative Attacker Attractiveness + raa := technicalAsset.RAA + var attackerAttractivenessLabel string + if technicalAsset.OutOfScope { + attackerAttractivenessLabel = "RAA: out of scope" + } else { + attackerAttractivenessLabel = "RAA: " + fmt.Sprintf("%.0f", raa) + " %" + } - return " " + hash(technicalAsset.Id) + ` [ - label=<
` + lineBreak + technicalAsset.Technologies.String() + `
` + technicalAsset.Size.String() + `
` + encode(title) + `
` + attackerAttractivenessLabel + `
> - shape=` + shape + ` style="` + determineShapeBorderLineStyle(technicalAsset) + `,` + determineShapeStyle(technicalAsset) + `" penwidth="` + determineShapeBorderPenWidth(technicalAsset, parsedModel) + `" fillcolor="` + determineShapeFillColor(technicalAsset, parsedModel) + `" - peripheries=` + strconv.Itoa(determineShapePeripheries(technicalAsset)) + ` - color="` + determineShapeBorderColor(technicalAsset, parsedModel) + "\"\n ]; " + compartmentBorder := "0" + if technicalAsset.MultiTenant { + compartmentBorder = "1" } + + return " " + hash(technicalAsset.Id) + ` [ +label=<
` + lineBreak + technicalAsset.Technologies.String() + `
` + technicalAsset.Size.String() + `
` + encode(title) + `
` + attackerAttractivenessLabel + `
> +shape=` + shape + ` style="` + determineShapeBorderLineStyle(technicalAsset) + `,` + determineShapeStyle(technicalAsset) + `" penwidth="` + determineShapeBorderPenWidth(technicalAsset, parsedModel) + `" fillcolor="` + determineShapeFillColor(technicalAsset, parsedModel) + `" +peripheries=` + strconv.Itoa(determineShapePeripheries(technicalAsset)) + ` +color="` + determineShapeBorderColor(technicalAsset, parsedModel) + "\"\n ]; " } func determineShapeStyle(ta *types.TechnicalAsset) string { @@ -744,29 +768,6 @@ func determineShapeBorderColor(ta *types.TechnicalAsset, parsedModel *types.Mode } } return Black - /* - if what.Integrity == MissionCritical { - for _, dataFlow := range IncomingTechnicalCommunicationLinksMappedByTargetId[what.ID] { - if !dataFlow.Readonly && dataFlow.Authentication == NoneAuthentication { - return Red - } - } - } - - if what.Integrity == Critical { - for _, dataFlow := range IncomingTechnicalCommunicationLinksMappedByTargetId[what.ID] { - if !dataFlow.Readonly && dataFlow.Authentication == NoneAuthentication { - return Amber - } - } - } - - if len(what.DataAssetsProcessed) == 0 && len(what.DataAssetsStored) == 0 { - return Pink // pink, because it's strange when too many technical assets process no data... some are ok, but many in a diagram is a sign of model forgery... - } - - return Black - */ } func determineShapePeripheries(ta *types.TechnicalAsset) int { @@ -786,7 +787,6 @@ func determineShapeBorderLineStyle(ta *types.TechnicalAsset) string { // red when >= confidential data stored in unencrypted technical asset func determineTechnicalAssetLabelColor(ta *types.TechnicalAsset, model *types.Model) string { - // TODO: Just move into main.go and let the generated risk determine the color, don't duplicate the logic here // Check for red if ta.Integrity == types.MissionCritical { return Red @@ -816,29 +816,6 @@ func determineTechnicalAssetLabelColor(ta *types.TechnicalAsset, model *types.Mo } } return Black - /* - if what.Encrypted { - return Black - } else { - if what.Confidentiality == StrictlyConfidential { - return Red - } - for _, storedDataAsset := range what.DataAssetsStored { - if ParsedModelRoot.DataAssets[storedDataAsset].Confidentiality == StrictlyConfidential { - return Red - } - } - if what.Confidentiality == Confidential { - return Amber - } - for _, storedDataAsset := range what.DataAssetsStored { - if ParsedModelRoot.DataAssets[storedDataAsset].Confidentiality == Confidential { - return Amber - } - } - return Black - } - */ } func GenerateDataAssetDiagramGraphvizImage(dotFile *os.File, targetDir string, diff --git a/pkg/server/server.go b/pkg/server/server.go index 36d91e73..bd5eb6db 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -58,6 +58,7 @@ type serverConfigReader interface { GetMaxGraphvizDPI() int GetBackupHistoryFilesToKeep() int GetAddModelTitle() bool + GetAddLegend() bool GetKeepDiagramSourceFiles() bool GetIgnoreOrphanedRiskTracking() bool GetThreagileVersion() string