diff --git a/cmd/raa/main.go b/cmd/raa/main.go index 44833820..e8f3e388 100644 --- a/cmd/raa/main.go +++ b/cmd/raa/main.go @@ -1,9 +1,9 @@ package main import ( - "encoding/json" "flag" "fmt" + "gopkg.in/yaml.v3" "io" "os" "sort" @@ -34,23 +34,23 @@ func main() { } } - // _ = os.WriteFile("raa_in.json", data, 0644) + // _ = os.WriteFile("raa_in.yaml", data, 0644) var input types.Model - parseError := json.Unmarshal(data, &input) + parseError := yaml.Unmarshal(data, &input) if parseError != nil { _, _ = fmt.Fprintf(os.Stderr, "failed to parse model: %v\n", parseError) os.Exit(-2) } text := CalculateRAA(&input) - outData, marshalError := json.MarshalIndent(input, "", " ") + outData, marshalError := yaml.Marshal(input) if marshalError != nil { _, _ = fmt.Fprintf(os.Stderr, "failed to print model: %v\n", marshalError) os.Exit(-2) } - // _ = os.WriteFile("raa_out.json", outData, 0644) + // _ = os.WriteFile("raa_out.yaml", outData, 0644) var outputFile io.Writer = os.Stdout if len(*outputFilename) > 0 { diff --git a/cmd/script/main.go b/cmd/script/main.go index 10a2d0e0..9b6d5ce6 100644 --- a/cmd/script/main.go +++ b/cmd/script/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "github.com/threagile/threagile/pkg/script" + "github.com/threagile/threagile/pkg/security/risks" "github.com/threagile/threagile/pkg/security/types" "gopkg.in/yaml.v3" "os" @@ -10,6 +11,14 @@ import ( ) func main() { + rules, loadError := risks.GetScriptRiskRules() + if loadError != nil { + fmt.Printf("error loading risk rules: %v\n", loadError) + return + } + + _ = rules + scriptFilename := filepath.Clean(filepath.Join("test", "risk-category.yaml")) ruleData, ruleReadError := os.ReadFile(scriptFilename) if ruleReadError != nil { diff --git a/pkg/model/custom-risk-category.go b/pkg/model/custom-risk-category.go index b762f340..9f379866 100644 --- a/pkg/model/custom-risk-category.go +++ b/pkg/model/custom-risk-category.go @@ -2,7 +2,6 @@ package model import ( "fmt" - "github.com/threagile/threagile/pkg/security/risks" "strings" "github.com/threagile/threagile/pkg/security/types" @@ -46,9 +45,9 @@ func (what *CustomRiskCategory) GenerateRisks(parsedModel *types.Model) ([]*type return generatedRisks, nil } -func LoadCustomRiskRules(pluginFiles []string, reporter types.ProgressReporter) risks.RiskRules { +func LoadCustomRiskRules(pluginFiles []string, reporter types.ProgressReporter) types.RiskRules { customRiskRuleList := make([]string, 0) - customRiskRules := make(risks.RiskRules) + customRiskRules := make(types.RiskRules) if len(pluginFiles) > 0 { reporter.Info("Loading custom risk rules:", strings.Join(pluginFiles, ", ")) diff --git a/pkg/model/parse.go b/pkg/model/parse.go index ef360da8..5be9a701 100644 --- a/pkg/model/parse.go +++ b/pkg/model/parse.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/threagile/threagile/pkg/common" "github.com/threagile/threagile/pkg/input" - "github.com/threagile/threagile/pkg/security/risks" "github.com/threagile/threagile/pkg/security/types" "path/filepath" "regexp" @@ -12,7 +11,7 @@ import ( "time" ) -func ParseModel(config *common.Config, modelInput *input.Model, builtinRiskRules risks.RiskRules, customRiskRules risks.RiskRules) (*types.Model, error) { +func ParseModel(config *common.Config, modelInput *input.Model, builtinRiskRules types.RiskRules, customRiskRules types.RiskRules) (*types.Model, error) { technologies := make(types.TechnologyMap) technologiesLoadError := technologies.LoadWithConfig(config, "technologies.yaml") if technologiesLoadError != nil { diff --git a/pkg/model/parse_test.go b/pkg/model/parse_test.go index 7353a844..bc4103e4 100644 --- a/pkg/model/parse_test.go +++ b/pkg/model/parse_test.go @@ -12,12 +12,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/threagile/threagile/pkg/input" - "github.com/threagile/threagile/pkg/security/risks" "github.com/threagile/threagile/pkg/security/types" ) func TestDefaultInputNotFail(t *testing.T) { - parsedModel, err := ParseModel(&common.Config{}, createInputModel(make(map[string]input.TechnicalAsset), make(map[string]input.DataAsset)), make(risks.RiskRules), make(risks.RiskRules)) + parsedModel, err := ParseModel(&common.Config{}, createInputModel(make(map[string]input.TechnicalAsset), make(map[string]input.DataAsset)), make(types.RiskRules), make(types.RiskRules)) assert.NoError(t, err) assert.NotNil(t, parsedModel) @@ -27,7 +26,7 @@ func TestInferConfidentiality_NotSet_NoOthers_ExpectTODO(t *testing.T) { ta := make(map[string]input.TechnicalAsset) da := make(map[string]input.DataAsset) - _, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(risks.RiskRules), make(risks.RiskRules)) + _, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(types.RiskRules), make(types.RiskRules)) // TODO: rename test and check if everyone agree that by default it should be public if there are no other assets assert.NoError(t, err) @@ -58,7 +57,7 @@ func TestInferConfidentiality_ExpectHighestConfidentiality(t *testing.T) { taWithPublicConfidentialityDataAsset.DataAssetsProcessed = append(taWithPublicConfidentialityDataAsset.DataAssetsProcessed, daPublicConfidentiality.ID) ta[taWithPublicConfidentialityDataAsset.ID] = taWithPublicConfidentialityDataAsset - parsedModel, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(risks.RiskRules), make(risks.RiskRules)) + parsedModel, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(types.RiskRules), make(types.RiskRules)) assert.NoError(t, err) assert.Equal(t, types.Confidential, parsedModel.TechnicalAssets[taWithConfidentialConfidentialityDataAsset.ID].Confidentiality) @@ -70,7 +69,7 @@ func TestInferIntegrity_NotSet_NoOthers_ExpectTODO(t *testing.T) { ta := make(map[string]input.TechnicalAsset) da := make(map[string]input.DataAsset) - _, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(risks.RiskRules), make(risks.RiskRules)) + _, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(types.RiskRules), make(types.RiskRules)) // TODO: rename test and check if everyone agree that by default it should be public if there are no other assets assert.NoError(t, err) @@ -101,7 +100,7 @@ func TestInferIntegrity_ExpectHighestIntegrity(t *testing.T) { taWithArchiveIntegrityDataAsset.DataAssetsProcessed = append(taWithArchiveIntegrityDataAsset.DataAssetsProcessed, daArchiveIntegrity.ID) ta[taWithArchiveIntegrityDataAsset.ID] = taWithArchiveIntegrityDataAsset - parsedModel, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(risks.RiskRules), make(risks.RiskRules)) + parsedModel, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(types.RiskRules), make(types.RiskRules)) assert.NoError(t, err) assert.Equal(t, types.Critical, parsedModel.TechnicalAssets[taWithCriticalIntegrityDataAsset.ID].Integrity) @@ -113,7 +112,7 @@ func TestInferAvailability_NotSet_NoOthers_ExpectTODO(t *testing.T) { ta := make(map[string]input.TechnicalAsset) da := make(map[string]input.DataAsset) - _, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(risks.RiskRules), make(risks.RiskRules)) + _, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(types.RiskRules), make(types.RiskRules)) assert.NoError(t, err) } @@ -143,7 +142,7 @@ func TestInferAvailability_ExpectHighestAvailability(t *testing.T) { taWithArchiveAvailabilityDataAsset.DataAssetsProcessed = append(taWithArchiveAvailabilityDataAsset.DataAssetsProcessed, daArchiveAvailability.ID) ta[taWithArchiveAvailabilityDataAsset.ID] = taWithArchiveAvailabilityDataAsset - parsedModel, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(risks.RiskRules), make(risks.RiskRules)) + parsedModel, err := ParseModel(&common.Config{}, createInputModel(ta, da), make(types.RiskRules), make(types.RiskRules)) assert.NoError(t, err) assert.Equal(t, types.Critical, parsedModel.TechnicalAssets[taWithCriticalAvailabilityDataAsset.ID].Availability) diff --git a/pkg/model/read.go b/pkg/model/read.go index 8259a18c..bcc53867 100644 --- a/pkg/model/read.go +++ b/pkg/model/read.go @@ -15,8 +15,8 @@ type ReadResult struct { ModelInput *input.Model ParsedModel *types.Model IntroTextRAA string - BuiltinRiskRules risks.RiskRules - CustomRiskRules risks.RiskRules + BuiltinRiskRules types.RiskRules + CustomRiskRules types.RiskRules } func (what ReadResult) ExplainRisk(cfg *common.Config, risk string, reporter common.DefaultProgressReporter) error { @@ -73,7 +73,7 @@ func ReadAndAnalyzeModel(config *common.Config, progressReporter types.ProgressR }, nil } -func applyRiskGeneration(parsedModel *types.Model, rules risks.RiskRules, +func applyRiskGeneration(parsedModel *types.Model, rules types.RiskRules, skipRiskRules []string, progressReporter types.ProgressReporter) { progressReporter.Info("Applying risk generation") diff --git a/pkg/model/runner.go b/pkg/model/runner.go index e14adf63..60a7dbc1 100644 --- a/pkg/model/runner.go +++ b/pkg/model/runner.go @@ -4,8 +4,8 @@ package model import ( "bytes" - "encoding/json" "fmt" + "gopkg.in/yaml.v3" "os" "os/exec" ) @@ -61,9 +61,9 @@ func (p *runner) Run(in any, out any, parameters ...string) error { return startError } - inData, inError := json.MarshalIndent(p.In, "", " ") + inData, inError := yaml.Marshal(p.In) if inError != nil { - return inError + return fmt.Errorf("error encoding input data: %w", inError) } _, writeError := stdin.Write(inData) @@ -83,10 +83,12 @@ func (p *runner) Run(in any, out any, parameters ...string) error { } stdout := stdoutBuf.Bytes() - unmarshalError := json.Unmarshal(stdout, &p.Out) + unmarshalError := yaml.Unmarshal(stdout, p.Out) if unmarshalError != nil { return unmarshalError } + // _ = os.WriteFile(fmt.Sprintf("%v.yaml", rand.Int31()), stdout, 0644) + return nil } diff --git a/pkg/report/report.go b/pkg/report/report.go index 017311c7..521ee4a4 100644 --- a/pkg/report/report.go +++ b/pkg/report/report.go @@ -57,7 +57,7 @@ func (r *pdfReporter) WriteReportPDF(reportFilename string, buildTimestamp string, modelHash string, introTextRAA string, - customRiskRules risks.RiskRules, + customRiskRules types.RiskRules, tempFolder string, model *types.Model) error { defer func() { @@ -4001,7 +4001,7 @@ func (r *pdfReporter) createSharedRuntimes(parsedModel *types.Model) { } } -func (r *pdfReporter) createRiskRulesChecked(parsedModel *types.Model, modelFilename string, skipRiskRules []string, buildTimestamp string, modelHash string, customRiskRules risks.RiskRules) { +func (r *pdfReporter) createRiskRulesChecked(parsedModel *types.Model, modelFilename string, skipRiskRules []string, buildTimestamp string, modelHash string, customRiskRules types.RiskRules) { r.pdf.SetTextColor(0, 0, 0) title := "Risk Rules Checked by Threagile" r.addHeadline(title, false) diff --git a/pkg/script/common/scope.go b/pkg/script/common/scope.go index c6b54e3a..fb0bd2ae 100644 --- a/pkg/script/common/scope.go +++ b/pkg/script/common/scope.go @@ -38,12 +38,12 @@ func (what *Scope) Init(risk *types.RiskCategory, methods map[string]Statement) func (what *Scope) SetModel(model *types.Model) error { if model != nil { - data, marshalError := json.Marshal(model) + data, marshalError := yaml.Marshal(model) if marshalError != nil { return marshalError } - unmarshalError := json.Unmarshal(data, &what.Model) + unmarshalError := yaml.Unmarshal(data, &what.Model) if unmarshalError != nil { return unmarshalError } diff --git a/pkg/script/risk-rule.go b/pkg/script/risk-rule.go index a7981859..3f260dfa 100644 --- a/pkg/script/risk-rule.go +++ b/pkg/script/risk-rule.go @@ -1,16 +1,18 @@ package script import ( + "embed" "fmt" "github.com/threagile/threagile/pkg/input" - "github.com/threagile/threagile/pkg/security/risks" "github.com/threagile/threagile/pkg/security/types" "gopkg.in/yaml.v3" + "io/fs" + "path/filepath" "strings" ) type RiskRule struct { - risks.RiskRule + types.RiskRule category types.RiskCategory supportedTags []string script *Script @@ -81,3 +83,31 @@ func (what *RiskRule) GenerateRisks(parsedModel *types.Model) ([]*types.Risk, er return newRisks, nil } + +func (what *RiskRule) Load(fileSystem embed.FS, path string, entry fs.DirEntry) error { + if entry.IsDir() { + return nil + } + + loadError := what.loadRiskRule(fileSystem, path) + if loadError != nil { + return loadError + } + + return nil +} + +func (what *RiskRule) loadRiskRule(fileSystem embed.FS, filename string) error { + scriptFilename := filepath.Clean(filename) + ruleData, ruleReadError := fileSystem.ReadFile(scriptFilename) + if ruleReadError != nil { + return fmt.Errorf("error reading risk category: %w\n", ruleReadError) + } + + _, parseError := what.ParseFromData(ruleData) + if parseError != nil { + return fmt.Errorf("error parsing scripts from %q: %w\n", scriptFilename, parseError) + } + + return nil +} diff --git a/pkg/security/risks/risks.go b/pkg/security/risks/risks.go index 6ee80db9..079ad696 100644 --- a/pkg/security/risks/risks.go +++ b/pkg/security/risks/risks.go @@ -1,12 +1,17 @@ package risks import ( + "embed" + "fmt" + "github.com/threagile/threagile/pkg/script" "github.com/threagile/threagile/pkg/security/risks/builtin" + "github.com/threagile/threagile/pkg/security/types" + "io/fs" ) -func GetBuiltInRiskRules() RiskRules { - rules := make(RiskRules) - for _, rule := range []RiskRule{ +func GetBuiltInRiskRules() types.RiskRules { + rules := make(types.RiskRules) + for _, rule := range []types.RiskRule{ builtin.NewAccidentalSecretLeakRule(), builtin.NewCodeBackdooringRule(), builtin.NewContainerBaseImageBackdooringRule(), @@ -53,5 +58,57 @@ func GetBuiltInRiskRules() RiskRules { rules[rule.Category().ID] = rule } + scriptRules, scriptError := GetScriptRiskRules() + if scriptError != nil { + fmt.Printf("error loading script risk rules: %v\n", scriptError) + return rules + } + + for id, rule := range scriptRules { + builtinRule, ok := rules[id] + if ok && builtinRule != nil { + fmt.Printf("WARNING: script risk rule %q shadows built-in risk rule\n", id) + } + + rules[id] = rule + } + return rules } + +//go:embed scripts/*.yaml +var ruleScripts embed.FS + +type RiskRules types.RiskRules + +func GetScriptRiskRules() (RiskRules, error) { + return make(RiskRules).LoadRiskRules() +} + +func (what RiskRules) LoadRiskRules() (RiskRules, error) { + fileSystem := ruleScripts + walkError := fs.WalkDir(fileSystem, "scripts", func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + + newRule := new(script.RiskRule).Init() + loadError := newRule.Load(fileSystem, path, entry) + if loadError != nil { + return loadError + } + + if newRule.Category().ID == "" { + return nil + } + + what[newRule.Category().ID] = newRule + return nil + }) + + if walkError != nil { + return nil, walkError + } + + return what, nil +} diff --git a/pkg/security/risks/scripts/accidental-secret-leak.yaml b/pkg/security/risks/scripts/accidental-secret-leak.yaml new file mode 100644 index 00000000..f3d840ab --- /dev/null +++ b/pkg/security/risks/scripts/accidental-secret-leak.yaml @@ -0,0 +1,140 @@ +id: accidental-secret-leak +title: Accidental Secret Leak +function: operations +stride: information-disclosure +cwe: 200 +description: + Sourcecode repositories (including their histories) as well as artifact registries can accidentally contain + secrets like checked-in or packaged-in passwords, API tokens, certificates, crypto keys, etc. +impact: + If this risk is unmitigated, attackers which have access to affected sourcecode repositories or artifact + registries might find secrets accidentally checked-in. +asvs: + V14 - Configuration Verification Requirements +cheat_sheet: + https://cheatsheetseries.owasp.org/cheatsheets/Attack_Surface_Analysis_Cheat_Sheet.html +action: + Build Pipeline Hardening +mitigation: + Establish measures preventing accidental check-in or package-in of secrets into sourcecode repositories and + artifact registries. This starts by using good .gitignore and .dockerignore files, but does not stop there. + See for example tools like \"git-secrets\" or \"Talisman\" to have check-in preventive measures for + secrets. Consider also to regularly scan your repositories for secrets accidentally checked-in using + scanning tools like "gitleaks" or "gitrob". +check: + Are recommendations from the linked cheat sheet and referenced ASVS chapter applied? +detection_logic: + In-scope sourcecode repositories and artifact registries. +risk_assessment: + The risk rating depends on the sensitivity of the technical asset itself and of the data assets processed. +false_positives: + Usually no false positives. +is_built_in: true + +script: + risk: + parameter: tech_asset + id: "{$risk.id}@{tech_asset.id}" + title: "get_title({tech_asset})" + severity: "calculate_severity(unlikely, get_impact({tech_asset}))" + exploitation_likelihood: unlikely + exploitation_impact: "get_impact({tech_asset})" + data_breach_probability: probable + data_breach_technical_assets: + - "{tech_asset.id}" + most_relevant_data_asset: "{tech_asset.id}" + + match: + parameter: tech_asset + do: + - if: + and: + - false: "{tech_asset.out_of_scope}" + - any: + in: "{tech_asset.technologies}" + or: + - true: "{.attributes.sourcecode-repository}" + - true: "{.attributes.artifact-registry}" + then: + return: true + + utils: + get_title: + parameters: + - tech_asset + do: + - if: + contains: + item: git + in: "{tech_asset.tags}" + then: + - return: + "Accidental Secret Leak(Git) risk at {tech_asset.title}: Git Leak Prevention" + else: + - return: + "Accidental Secret Leak risk at {tech_asset.title}" + + get_impact: + parameters: + - tech_asset + do: + - assign: + - impact: low + - highest_confidentiality: "get_highest({tech_asset}, confidentiality)" + - highest_integrity: "get_highest({tech_asset}, integrity)" + - highest_availability: "get_highest({tech_asset}, availability)" + - if: + or: + - equal-or-greater: + as: confidentiality + first: "{highest_confidentiality}" + second: confidential + - equal-or-greater: + as: integrity + first: "{highest_integrity}" + second: critical + - equal-or-greater: + as: availability + first: "{highest_availability}" + second: critical + then: + - assign: + impact: medium + - if: + or: + - equal-or-greater: + as: confidentiality + first: "{highest_confidentiality}" + second: strictly-confidential + - equal-or-greater: + as: integrity + first: "{highest_integrity}" + second: mission-critical + - equal-or-greater: + as: availability + first: "{highest_availability}" + second: mission-critical + then: + - assign: + impact: high + - return: "{impact}" + + get_highest: + parameters: + - tech_asset + - "type" + do: + - assign: + - value: "{tech_asset.{type}}" + - loop: + in: "{tech_asset.data_assets_processed}" + item: data_id + do: + if: + greater: + first: "{$model.data_assets.{data_id}.{type}}" + second: "{value}" + then: + - assign: + value: "{$model.data_assets.{data_id}.{type}}" + - return: "{value}" diff --git a/pkg/security/risks/risk-rule.go b/pkg/security/types/risk-rule.go similarity index 58% rename from pkg/security/risks/risk-rule.go rename to pkg/security/types/risk-rule.go index d3a00306..f7be5eb4 100644 --- a/pkg/security/risks/risk-rule.go +++ b/pkg/security/types/risk-rule.go @@ -1,11 +1,9 @@ -package risks - -import "github.com/threagile/threagile/pkg/security/types" +package types type RiskRule interface { - Category() *types.RiskCategory + Category() *RiskCategory SupportedTags() []string - GenerateRisks(*types.Model) ([]*types.Risk, error) + GenerateRisks(*Model) ([]*Risk, error) } type RiskRules map[string]RiskRule diff --git a/pkg/security/types/technical_asset.go b/pkg/security/types/technical_asset.go index 8e5691ce..5a2bdc9b 100644 --- a/pkg/security/types/technical_asset.go +++ b/pkg/security/types/technical_asset.go @@ -37,8 +37,7 @@ type TechnicalAsset struct { DataFormatsAccepted []DataFormat `json:"data_formats_accepted,omitempty" yaml:"data_formats_accepted,omitempty"` CommunicationLinks []*CommunicationLink `json:"communication_links,omitempty" yaml:"communication_links,omitempty"` DiagramTweakOrder int `json:"diagram_tweak_order,omitempty" yaml:"diagram_tweak_order,omitempty"` - // will be set by separate calculation step: - RAA float64 `json:"raa,omitempty" yaml:"raa,omitempty"` + RAA float64 `json:"raa,omitempty" yaml:"raa,omitempty"` // will be set by separate calculation step } func (what TechnicalAsset) IsTaggedWithAny(tags ...string) bool { diff --git a/pkg/server/server.go b/pkg/server/server.go index 201ba8ec..68890aa4 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -35,7 +35,7 @@ type server struct { mapFolderNameToTokenHash map[string]string extremeShortTimeoutsForTesting bool locksByFolderName map[string]*sync.Mutex - customRiskRules risks.RiskRules + customRiskRules types.RiskRules } func RunServer(config *common.Config) {