Skip to content

Commit 52ed5f3

Browse files
author
Yevhen Zavhorodnii
committed
Allow to add legend to diagram
1 parent 7eeb37f commit 52ed5f3

File tree

6 files changed

+109
-118
lines changed

6 files changed

+109
-118
lines changed

docs/config.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ This config keys is used when application run in [analyze mode](./mode-analyze.m
4545
| `DiagramDPI` | int | The same as `-diagram-dpi` [flags](./flags.md) | see [flags](./flags.md) |
4646
| `GraphvizDPI` | TBD | The same as `-verbose` or `--v` at [flags](./flags.md) | see [flags](./flags.md) |
4747
| `MaxGraphvizDPI` | TBD | The same as `-verbose` or `--v` at [flags](./flags.md) | see [flags](./flags.md) |
48-
| `AddModelTitle` | TBD | Identify if model title shall be added to diagram | false |
48+
| `AddModelTitle` | TBD | Identify if model title shall be added to diagram | false |
49+
| `AddLegend` | TBD | Identify if legend shall be added to diagram | false |
4950

5051
### Excel config keys
5152

internal/threagile/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type Config struct {
6060
BackupHistoryFilesToKeepValue int `json:"BackupHistoryFilesToKeep,omitempty" yaml:"BackupHistoryFilesToKeep"`
6161

6262
AddModelTitleValue bool `json:"AddModelTitle,omitempty" yaml:"AddModelTitle"`
63+
AddLegendValue bool `json:"AddLegend,omitempty" yaml:"AddLegend"`
6364
KeepDiagramSourceFilesValue bool `json:"KeepDiagramSourceFiles,omitempty" yaml:"KeepDiagramSourceFiles"`
6465
IgnoreOrphanedRiskTrackingValue bool `json:"IgnoreOrphanedRiskTracking,omitempty" yaml:"IgnoreOrphanedRiskTracking"`
6566

@@ -120,6 +121,7 @@ type ConfigGetter interface {
120121
GetMaxGraphvizDPI() int
121122
GetBackupHistoryFilesToKeep() int
122123
GetAddModelTitle() bool
124+
GetAddLegend() bool
123125
GetKeepDiagramSourceFiles() bool
124126
GetIgnoreOrphanedRiskTracking() bool
125127
GetSkipDataFlowDiagram() bool
@@ -204,6 +206,7 @@ func (c *Config) Defaults(buildTimestamp string) *Config {
204206
BackupHistoryFilesToKeepValue: DefaultBackupHistoryFilesToKeep,
205207

206208
AddModelTitleValue: false,
209+
AddLegendValue: false,
207210
KeepDiagramSourceFilesValue: false,
208211
IgnoreOrphanedRiskTrackingValue: false,
209212

@@ -474,6 +477,9 @@ func (c *Config) Merge(config Config, values map[string]any) {
474477
case strings.ToLower("AddModelTitle"):
475478
c.AddModelTitleValue = config.AddModelTitleValue
476479

480+
case strings.ToLower("AddLegend"):
481+
c.AddLegendValue = config.AddLegendValue
482+
477483
case strings.ToLower("KeepDiagramSourceFiles"):
478484
c.KeepDiagramSourceFilesValue = config.KeepDiagramSourceFilesValue
479485

@@ -772,6 +778,10 @@ func (c *Config) GetAddModelTitle() bool {
772778
return c.AddModelTitleValue
773779
}
774780

781+
func (c *Config) GetAddLegend() bool {
782+
return c.AddLegendValue
783+
}
784+
775785
func (c *Config) GetKeepDiagramSourceFiles() bool {
776786
return c.KeepDiagramSourceFilesValue
777787
}

pkg/model/read.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type configReader interface {
6565
GetMaxGraphvizDPI() int
6666
GetBackupHistoryFilesToKeep() int
6767
GetAddModelTitle() bool
68+
GetAddLegend() bool
6869
GetKeepDiagramSourceFiles() bool
6970
GetIgnoreOrphanedRiskTracking() bool
7071
GetThreagileVersion() string

pkg/report/generate.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type reportConfigReader interface {
7676

7777
GetKeepDiagramSourceFiles() bool
7878
GetAddModelTitle() bool
79+
GetAddLegend() bool
7980
GetReportConfigurationHideChapters() map[ChaptersToShowHide]bool
8081
}
8182

@@ -117,7 +118,7 @@ func Generate(config reportConfigReader, readResult *model.ReadResult, commands
117118
gvFile = tmpFileGV.Name()
118119
defer func() { _ = os.Remove(gvFile) }()
119120
}
120-
dotFile, err := WriteDataFlowDiagramGraphvizDOT(readResult.ParsedModel, gvFile, diagramDPI, config.GetAddModelTitle(), progressReporter)
121+
dotFile, err := WriteDataFlowDiagramGraphvizDOT(readResult.ParsedModel, gvFile, diagramDPI, config.GetAddModelTitle(), config.GetAddLegend(), progressReporter)
121122
if err != nil {
122123
return fmt.Errorf("error while generating data flow diagram: %w", err)
123124
}

pkg/report/graphviz.go

Lines changed: 93 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,61 @@ import (
1515
)
1616

1717
func WriteDataFlowDiagramGraphvizDOT(parsedModel *types.Model,
18-
diagramFilenameDOT string, dpi int, addModelTitle bool,
18+
diagramFilenameDOT string, dpi int, addModelTitle bool, addLegend bool,
1919
progressReporter progressReporter) (*os.File, error) {
2020
progressReporter.Info("Writing data flow diagram input")
2121

2222
var dotContent strings.Builder
2323
dotContent.WriteString("digraph generatedModel { concentrate=false \n")
2424

25+
if addLegend {
26+
dotContent.WriteString("subgraph cluster_shape_legend { \n")
27+
dotContent.WriteString(" label=\"Shape legend\"; \n")
28+
dotContent.WriteString(" color=\"lightgrey\"; \n")
29+
dotContent.WriteString(" style=\"dashed\"; \n")
30+
dotContent.WriteString(makeLegendNode("external_entity_item", "External Entity", "0", "black", "box", "solid", "filled", "2.0", VeryLightGray, "1", Black))
31+
dotContent.WriteString(makeLegendNode("process_item", "Process", "0", "black", "ellipse", "solid", "filled", "2.0", VeryLightGray, "1", Black))
32+
dotContent.WriteString(makeLegendNode("datastore_item", "Datastore", "0", "black", "cylinder", "solid", "filled", "2.0", VeryLightGray, "1", Black))
33+
dotContent.WriteString(makeLegendNode("used_as_client_item", "Used as client", "0", "black", "octagon", "solid", "filled", "2.0", VeryLightGray, "1", Black))
34+
dotContent.WriteString("} \n")
35+
36+
dotContent.WriteString("subgraph cluster_tenant_legend { \n")
37+
dotContent.WriteString(" label=\"Tenant legend\"; \n")
38+
dotContent.WriteString(" color=\"lightgrey\"; \n")
39+
dotContent.WriteString(" style=\"dashed\"; \n")
40+
dotContent.WriteString(makeLegendNode("single_tenant", "Single tenant", "0", "black", "box", "solid", "filled", "2.0", VeryLightGray, "1", Black))
41+
dotContent.WriteString(makeLegendNode("multi_tenant", "Multitenant", "1", "black", "box", "solid", "filled", "2.0", VeryLightGray, "1", Black))
42+
dotContent.WriteString("} \n")
43+
44+
dotContent.WriteString("subgraph cluster_label_legend { \n")
45+
dotContent.WriteString(" label=\"Label color legend\"; \n")
46+
dotContent.WriteString(" color=\"lightgrey\"; \n")
47+
dotContent.WriteString(" style=\"dashed\"; \n")
48+
dotContent.WriteString(makeLegendNode("mission_critical", "Mission Critical Asset", "0", Red, "box", "solid", "filled", "3.0", VeryLightGray, "1", Red))
49+
dotContent.WriteString(makeLegendNode("critical", "Critical Asset", "0", Amber, "box", "solid", "filled", "3.0", VeryLightGray, "1", Amber))
50+
dotContent.WriteString(makeLegendNode("other", "Important and Other Assets", "0", Black, "box", "solid", "filled", "2.0", VeryLightGray, "1", Black))
51+
dotContent.WriteString("} \n")
52+
53+
dotContent.WriteString("subgraph cluster_border_line_legend { \n")
54+
dotContent.WriteString(" label=\"Label color legend\"; \n")
55+
dotContent.WriteString(" color=\"lightgrey\"; \n")
56+
dotContent.WriteString(" style=\"dashed\"; \n")
57+
dotContent.WriteString(makeLegendNode("dotted", "Model forgery attempt", "0", Black, "box", "dotted ", "filled", "2.0", VeryLightGray, "1", Black))
58+
dotContent.WriteString(makeLegendNode("solid", "Normal", "0", Black, "box", "solid", "filled", "2.0", VeryLightGray, "1", Black))
59+
dotContent.WriteString("} \n")
60+
61+
dotContent.WriteString("subgraph cluster_fill_legend { \n")
62+
dotContent.WriteString(" label=\"Shape fill legend (darker for physical machines, brighter for container and even more brighter for serverless\"; \n")
63+
dotContent.WriteString(" color=\"lightgrey\"; \n")
64+
dotContent.WriteString(" style=\"dashed\"; \n")
65+
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))
66+
dotContent.WriteString(makeLegendNode("internet", "Asset used over the internet", "0", "black", "box", "solid", "filled", "2.0", ExtremeLightBlue, "1", Black))
67+
dotContent.WriteString(makeLegendNode("out_of_scope", "Out of scope", "0", "black", "box", "solid", "filled", "2.0", OutOfScopeFancy, "1", Black))
68+
dotContent.WriteString(makeLegendNode("custom_developed_part", "Custom developed part", "0", "black", "box", "solid", "filled", "2.0", CustomDevelopedParts, "1", Black))
69+
dotContent.WriteString(makeLegendNode("other_assets", "Other assets", "0", "black", "box", "solid", "filled", "2.0", VeryLightGray, "1", Black))
70+
dotContent.WriteString("} \n")
71+
}
72+
2573
// Metadata init ===============================================================================
2674
tweaks := ""
2775
if parsedModel.DiagramTweakNodesep > 0 {
@@ -252,11 +300,8 @@ func WriteDataFlowDiagramGraphvizDOT(parsedModel *types.Model,
252300
return nil, fmt.Errorf("error while making diagram same-rank node tweaks: %w", err)
253301
}
254302
dotContent.WriteString(diagramSameRankNodeTweaks)
255-
256303
dotContent.WriteString("}")
257304

258-
//fmt.Println(dotContent.String())
259-
260305
// Write the DOT file
261306
file, err := os.Create(filepath.Clean(diagramFilenameDOT))
262307
if err != nil {
@@ -270,6 +315,14 @@ func WriteDataFlowDiagramGraphvizDOT(parsedModel *types.Model,
270315
return file, nil
271316
}
272317

318+
func makeLegendNode(id, title, compartmentBorder, labelColor, shape, borderLineStyle, shapeStyle, borderPenWidth, shapeFillColor, shapePeripheries, shapeBorderColor string) string {
319+
return " " + id + ` [
320+
label=<<table border="0" cellborder="` + compartmentBorder + `" cellpadding="2" cellspacing="0"><tr><td><font point-size="15" color="` + DarkBlue + `">list of technologies` + `</font><br/><font point-size="15" color="` + LightGray + `">technical asset size</font></td></tr><tr><td><b><font color="` + labelColor + `">` + encode(title) + `</font></b><br/></td></tr><tr><td>attacker attractiveness level</td></tr></table>>
321+
shape=` + shape + ` style="` + borderLineStyle + `,` + shapeStyle + `" penwidth="` + borderPenWidth + `" fillcolor="` + shapeFillColor + `"
322+
peripheries=` + shapePeripheries + `
323+
color="` + shapeBorderColor + "\"\n ]; "
324+
}
325+
273326
// Pen Widths:
274327

275328
func determineArrowPenWidth(cl *types.CommunicationLink, parsedModel *types.Model) string {
@@ -362,35 +415,6 @@ func determineArrowColor(cl *types.CommunicationLink, parsedModel *types.Model)
362415
}
363416
// default
364417
return Black
365-
/*
366-
} else if dataFlow.Authentication != NoneAuthentication {
367-
return Black
368-
} else {
369-
// check for red
370-
for _, sentDataAsset := range dataFlow.DataAssetsSent { // first check if any red?
371-
if ParsedModelRoot.DataAssets[sentDataAsset].Integrity == MissionCritical {
372-
return Red
373-
}
374-
}
375-
for _, receivedDataAsset := range dataFlow.DataAssetsReceived { // first check if any red?
376-
if ParsedModelRoot.DataAssets[receivedDataAsset].Integrity == MissionCritical {
377-
return Red
378-
}
379-
}
380-
// check for amber
381-
for _, sentDataAsset := range dataFlow.DataAssetsSent { // then check if any amber?
382-
if ParsedModelRoot.DataAssets[sentDataAsset].Integrity == Critical {
383-
return Amber
384-
}
385-
}
386-
for _, receivedDataAsset := range dataFlow.DataAssetsReceived { // then check if any amber?
387-
if ParsedModelRoot.DataAssets[receivedDataAsset].Integrity == Critical {
388-
return Amber
389-
}
390-
}
391-
return Black
392-
}
393-
*/
394418
}
395419

396420
func GenerateDataFlowDiagramGraphvizImage(dotFile *os.File, targetDir string,
@@ -638,48 +662,48 @@ func makeTechAssetNode(parsedModel *types.Model, technicalAsset *types.Technical
638662
return " " + hash(technicalAsset.Id) + ` [ shape="box" style="filled" fillcolor="` + color + `"
639663
label=<<b>` + encode(technicalAsset.Title) + `</b>> penwidth="3.0" color="` + color + `" ];
640664
`
641-
} else {
642-
var shape, title string
643-
var lineBreak = ""
644-
switch technicalAsset.Type {
645-
case types.ExternalEntity:
646-
shape = "box"
647-
title = technicalAsset.Title
648-
case types.Process:
649-
shape = "ellipse"
650-
title = technicalAsset.Title
651-
case types.Datastore:
652-
shape = "cylinder"
653-
title = technicalAsset.Title
654-
if technicalAsset.Redundant {
655-
lineBreak = "<br/>"
656-
}
657-
}
665+
}
658666

659-
if technicalAsset.UsedAsClientByHuman {
660-
shape = "octagon"
667+
var shape, title string
668+
var lineBreak = ""
669+
switch technicalAsset.Type {
670+
case types.ExternalEntity:
671+
shape = "box"
672+
title = technicalAsset.Title
673+
case types.Process:
674+
shape = "ellipse"
675+
title = technicalAsset.Title
676+
case types.Datastore:
677+
shape = "cylinder"
678+
title = technicalAsset.Title
679+
if technicalAsset.Redundant {
680+
lineBreak = "<br/>"
661681
}
682+
}
662683

663-
// RAA = Relative Attacker Attractiveness
664-
raa := technicalAsset.RAA
665-
var attackerAttractivenessLabel string
666-
if technicalAsset.OutOfScope {
667-
attackerAttractivenessLabel = "<font point-size=\"15\" color=\"#603112\">RAA: out of scope</font>"
668-
} else {
669-
attackerAttractivenessLabel = "<font point-size=\"15\" color=\"#603112\">RAA: " + fmt.Sprintf("%.0f", raa) + " %</font>"
670-
}
684+
if technicalAsset.UsedAsClientByHuman {
685+
shape = "octagon"
686+
}
671687

672-
compartmentBorder := "0"
673-
if technicalAsset.MultiTenant {
674-
compartmentBorder = "1"
675-
}
688+
// RAA = Relative Attacker Attractiveness
689+
raa := technicalAsset.RAA
690+
var attackerAttractivenessLabel string
691+
if technicalAsset.OutOfScope {
692+
attackerAttractivenessLabel = "<font point-size=\"15\" color=\"#603112\">RAA: out of scope</font>"
693+
} else {
694+
attackerAttractivenessLabel = "<font point-size=\"15\" color=\"#603112\">RAA: " + fmt.Sprintf("%.0f", raa) + " %</font>"
695+
}
676696

677-
return " " + hash(technicalAsset.Id) + ` [
678-
label=<<table border="0" cellborder="` + compartmentBorder + `" cellpadding="2" cellspacing="0"><tr><td><font point-size="15" color="` + DarkBlue + `">` + lineBreak + technicalAsset.Technologies.String() + `</font><br/><font point-size="15" color="` + LightGray + `">` + technicalAsset.Size.String() + `</font></td></tr><tr><td><b><font color="` + determineTechnicalAssetLabelColor(technicalAsset, parsedModel) + `">` + encode(title) + `</font></b><br/></td></tr><tr><td>` + attackerAttractivenessLabel + `</td></tr></table>>
679-
shape=` + shape + ` style="` + determineShapeBorderLineStyle(technicalAsset) + `,` + determineShapeStyle(technicalAsset) + `" penwidth="` + determineShapeBorderPenWidth(technicalAsset, parsedModel) + `" fillcolor="` + determineShapeFillColor(technicalAsset, parsedModel) + `"
680-
peripheries=` + strconv.Itoa(determineShapePeripheries(technicalAsset)) + `
681-
color="` + determineShapeBorderColor(technicalAsset, parsedModel) + "\"\n ]; "
697+
compartmentBorder := "0"
698+
if technicalAsset.MultiTenant {
699+
compartmentBorder = "1"
682700
}
701+
702+
return " " + hash(technicalAsset.Id) + ` [
703+
label=<<table border="0" cellborder="` + compartmentBorder + `" cellpadding="2" cellspacing="0"><tr><td><font point-size="15" color="` + DarkBlue + `">` + lineBreak + technicalAsset.Technologies.String() + `</font><br/><font point-size="15" color="` + LightGray + `">` + technicalAsset.Size.String() + `</font></td></tr><tr><td><b><font color="` + determineTechnicalAssetLabelColor(technicalAsset, parsedModel) + `">` + encode(title) + `</font></b><br/></td></tr><tr><td>` + attackerAttractivenessLabel + `</td></tr></table>>
704+
shape=` + shape + ` style="` + determineShapeBorderLineStyle(technicalAsset) + `,` + determineShapeStyle(technicalAsset) + `" penwidth="` + determineShapeBorderPenWidth(technicalAsset, parsedModel) + `" fillcolor="` + determineShapeFillColor(technicalAsset, parsedModel) + `"
705+
peripheries=` + strconv.Itoa(determineShapePeripheries(technicalAsset)) + `
706+
color="` + determineShapeBorderColor(technicalAsset, parsedModel) + "\"\n ]; "
683707
}
684708

685709
func determineShapeStyle(ta *types.TechnicalAsset) string {
@@ -744,29 +768,6 @@ func determineShapeBorderColor(ta *types.TechnicalAsset, parsedModel *types.Mode
744768
}
745769
}
746770
return Black
747-
/*
748-
if what.Integrity == MissionCritical {
749-
for _, dataFlow := range IncomingTechnicalCommunicationLinksMappedByTargetId[what.ID] {
750-
if !dataFlow.Readonly && dataFlow.Authentication == NoneAuthentication {
751-
return Red
752-
}
753-
}
754-
}
755-
756-
if what.Integrity == Critical {
757-
for _, dataFlow := range IncomingTechnicalCommunicationLinksMappedByTargetId[what.ID] {
758-
if !dataFlow.Readonly && dataFlow.Authentication == NoneAuthentication {
759-
return Amber
760-
}
761-
}
762-
}
763-
764-
if len(what.DataAssetsProcessed) == 0 && len(what.DataAssetsStored) == 0 {
765-
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...
766-
}
767-
768-
return Black
769-
*/
770771
}
771772

772773
func determineShapePeripheries(ta *types.TechnicalAsset) int {
@@ -786,7 +787,6 @@ func determineShapeBorderLineStyle(ta *types.TechnicalAsset) string {
786787

787788
// red when >= confidential data stored in unencrypted technical asset
788789
func determineTechnicalAssetLabelColor(ta *types.TechnicalAsset, model *types.Model) string {
789-
// TODO: Just move into main.go and let the generated risk determine the color, don't duplicate the logic here
790790
// Check for red
791791
if ta.Integrity == types.MissionCritical {
792792
return Red
@@ -816,29 +816,6 @@ func determineTechnicalAssetLabelColor(ta *types.TechnicalAsset, model *types.Mo
816816
}
817817
}
818818
return Black
819-
/*
820-
if what.Encrypted {
821-
return Black
822-
} else {
823-
if what.Confidentiality == StrictlyConfidential {
824-
return Red
825-
}
826-
for _, storedDataAsset := range what.DataAssetsStored {
827-
if ParsedModelRoot.DataAssets[storedDataAsset].Confidentiality == StrictlyConfidential {
828-
return Red
829-
}
830-
}
831-
if what.Confidentiality == Confidential {
832-
return Amber
833-
}
834-
for _, storedDataAsset := range what.DataAssetsStored {
835-
if ParsedModelRoot.DataAssets[storedDataAsset].Confidentiality == Confidential {
836-
return Amber
837-
}
838-
}
839-
return Black
840-
}
841-
*/
842819
}
843820

844821
func GenerateDataAssetDiagramGraphvizImage(dotFile *os.File, targetDir string,

pkg/server/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type serverConfigReader interface {
5858
GetMaxGraphvizDPI() int
5959
GetBackupHistoryFilesToKeep() int
6060
GetAddModelTitle() bool
61+
GetAddLegend() bool
6162
GetKeepDiagramSourceFiles() bool
6263
GetIgnoreOrphanedRiskTracking() bool
6364
GetThreagileVersion() string

0 commit comments

Comments
 (0)