-
Notifications
You must be signed in to change notification settings - Fork 6
DONT MERGE: feat: add kosli attest sarif command
#838
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,223 @@ | ||||||
| package main | ||||||
|
|
||||||
| import ( | ||||||
| "fmt" | ||||||
| "io" | ||||||
| "net/http" | ||||||
| "net/url" | ||||||
| "os" | ||||||
|
|
||||||
| "github.com/kosli-dev/cli/internal/requests" | ||||||
| "github.com/kosli-dev/cli/internal/sarif" | ||||||
| "github.com/spf13/cobra" | ||||||
| ) | ||||||
|
|
||||||
| type SarifAttestationPayload struct { | ||||||
| *CommonAttestationPayload | ||||||
| SarifResults *sarif.SarifData `json:"sarif_results"` | ||||||
| Compliant bool `json:"is_compliant"` | ||||||
| } | ||||||
|
|
||||||
| type attestSarifOptions struct { | ||||||
| *CommonAttestationOptions | ||||||
| sarifFilePath string | ||||||
| uploadResultsFile bool | ||||||
| payload SarifAttestationPayload | ||||||
| } | ||||||
|
|
||||||
| const attestSarifShortDesc = `Report a SARIF attestation to an artifact or a trail in a Kosli flow. ` | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: trailing whitespace (two spaces before the closing backtick). Already flagged in the prior review — just confirming it's still present.
Suggested change
|
||||||
|
|
||||||
| const attestSarifLongDesc = attestSarifShortDesc + ` | ||||||
| Accepts SARIF v2.1.0 scan results from any compatible tool (e.g. Checkov, Trivy, Semgrep, Snyk, CodeQL). | ||||||
| The tool name and version are taken from the SARIF report's runs[0].tool.driver fields and shown in | ||||||
| the Kosli UI alongside the parsed findings. | ||||||
|
|
||||||
| The ^--scan-results^ .json file is analyzed and a summary of the scan results is reported to Kosli. | ||||||
|
|
||||||
| By default, the ^--scan-results^ .json file is also uploaded to Kosli's evidence vault. | ||||||
| You can disable that by setting ^--upload-results=false^. | ||||||
|
|
||||||
| Compliance is determined by the ^--compliant^ flag (default true). The CLI does not derive | ||||||
| compliance from the SARIF findings — the caller decides whether the scan should be treated | ||||||
| as compliant or not (e.g. based on its own policy or rego rules). | ||||||
| ` + attestationBindingDesc + ` | ||||||
|
|
||||||
| ` + commitDescription | ||||||
|
|
||||||
| const attestSarifExample = ` | ||||||
| # report a SARIF attestation about a trail (compliant by default): | ||||||
| kosli attest sarif \ | ||||||
| --name yourAttestationName \ | ||||||
| --flow yourFlowName \ | ||||||
| --trail yourTrailName \ | ||||||
| --scan-results yourScanSARIFResults \ | ||||||
| --api-token yourAPIToken \ | ||||||
| --org yourOrgName | ||||||
|
|
||||||
| # report a non-compliant SARIF attestation about a trail: | ||||||
| kosli attest sarif \ | ||||||
| --name yourAttestationName \ | ||||||
| --flow yourFlowName \ | ||||||
| --trail yourTrailName \ | ||||||
| --scan-results yourScanSARIFResults \ | ||||||
| --compliant=false \ | ||||||
| --api-token yourAPIToken \ | ||||||
| --org yourOrgName | ||||||
|
|
||||||
| # report a SARIF attestation about a pre-built docker artifact (kosli calculates the fingerprint): | ||||||
| kosli attest sarif yourDockerImageName \ | ||||||
| --artifact-type docker \ | ||||||
| --name yourAttestationName \ | ||||||
| --flow yourFlowName \ | ||||||
| --trail yourTrailName \ | ||||||
| --scan-results yourScanSARIFResults \ | ||||||
| --api-token yourAPIToken \ | ||||||
| --org yourOrgName | ||||||
|
|
||||||
| # report a SARIF attestation about a pre-built docker artifact (you provide the fingerprint): | ||||||
| kosli attest sarif \ | ||||||
| --fingerprint yourDockerImageFingerprint \ | ||||||
| --name yourAttestationName \ | ||||||
| --flow yourFlowName \ | ||||||
| --trail yourTrailName \ | ||||||
| --scan-results yourScanSARIFResults \ | ||||||
| --api-token yourAPIToken \ | ||||||
| --org yourOrgName | ||||||
|
|
||||||
| # report a SARIF attestation about an artifact which has not been reported yet in a trail: | ||||||
| kosli attest sarif \ | ||||||
| --name yourTemplateArtifactName.yourAttestationName \ | ||||||
| --flow yourFlowName \ | ||||||
| --trail yourTrailName \ | ||||||
| --commit yourArtifactGitCommit \ | ||||||
| --scan-results yourScanSARIFResults \ | ||||||
| --api-token yourAPIToken \ | ||||||
| --org yourOrgName | ||||||
|
|
||||||
| # report a SARIF attestation about a trail without uploading the results file: | ||||||
| kosli attest sarif \ | ||||||
| --name yourAttestationName \ | ||||||
| --flow yourFlowName \ | ||||||
| --trail yourTrailName \ | ||||||
| --scan-results yourScanSARIFResults \ | ||||||
| --upload-results=false \ | ||||||
| --api-token yourAPIToken \ | ||||||
| --org yourOrgName | ||||||
| ` | ||||||
|
|
||||||
| func newAttestSarifCmd(out io.Writer) *cobra.Command { | ||||||
| o := &attestSarifOptions{ | ||||||
| CommonAttestationOptions: &CommonAttestationOptions{ | ||||||
| fingerprintOptions: &fingerprintOptions{}, | ||||||
| }, | ||||||
| payload: SarifAttestationPayload{ | ||||||
| CommonAttestationPayload: &CommonAttestationPayload{}, | ||||||
| }, | ||||||
| } | ||||||
| cmd := &cobra.Command{ | ||||||
| Use: "sarif [IMAGE-NAME | FILE-PATH | DIR-PATH]", | ||||||
| Short: attestSarifShortDesc, | ||||||
| Long: attestSarifLongDesc, | ||||||
| Example: attestSarifExample, | ||||||
| PreRunE: func(cmd *cobra.Command, args []string) error { | ||||||
|
|
||||||
| err := CustomMaximumNArgs(1, args) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| err = RequireGlobalFlags(global, []string{"Org", "ApiToken"}) | ||||||
| if err != nil { | ||||||
| return ErrorBeforePrintingUsage(cmd, err.Error()) | ||||||
| } | ||||||
|
|
||||||
| err = MuXRequiredFlags(cmd, []string{"fingerprint", "artifact-type"}, false) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| err = ValidateSliceValues(o.redactedCommitInfo, allowedCommitRedactionValues) | ||||||
| if err != nil { | ||||||
| return fmt.Errorf("%s for --redact-commit-info", err.Error()) | ||||||
| } | ||||||
|
|
||||||
| err = ValidateAttestationArtifactArg(args, o.fingerprintOptions.artifactType, o.payload.ArtifactFingerprint) | ||||||
| if err != nil { | ||||||
| return ErrorBeforePrintingUsage(cmd, err.Error()) | ||||||
| } | ||||||
|
|
||||||
| return ValidateRegistryFlags(cmd, o.fingerprintOptions) | ||||||
|
|
||||||
| }, | ||||||
| RunE: func(cmd *cobra.Command, args []string) error { | ||||||
| o.repoURLExplicit = cmd.Flags().Changed("repo-url") | ||||||
| return o.run(args) | ||||||
| }, | ||||||
| } | ||||||
|
|
||||||
| ci := WhichCI() | ||||||
| addAttestationFlags(cmd, o.CommonAttestationOptions, o.payload.CommonAttestationPayload, ci) | ||||||
| cmd.Flags().StringVarP(&o.sarifFilePath, "scan-results", "R", "", sarifResultsFileFlag) | ||||||
| cmd.Flags().BoolVar(&o.uploadResultsFile, "upload-results", true, uploadSarifResultsFlag) | ||||||
| cmd.Flags().BoolVarP(&o.payload.Compliant, "compliant", "C", true, attestationCompliantFlag) | ||||||
|
|
||||||
| err := RequireFlags(cmd, []string{"flow", "trail", "name", "scan-results"}) | ||||||
| if err != nil { | ||||||
| logger.Error("failed to configure required flags: %v", err) | ||||||
| } | ||||||
|
|
||||||
| return cmd | ||||||
| } | ||||||
|
|
||||||
| func (o *attestSarifOptions) run(args []string) error { | ||||||
| url, err := url.JoinPath(global.Host, "api/v2/attestations", global.Org, o.flowName, "trail", o.trailName, "sarif") | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| err = o.CommonAttestationOptions.run(args, o.payload.CommonAttestationPayload) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| logger.Debug("parsing SARIF results file: %s", o.sarifFilePath) | ||||||
| o.payload.SarifResults, err = sarif.ProcessSarifResultFile(o.sarifFilePath) | ||||||
| if err != nil { | ||||||
| return fmt.Errorf("failed to parse SARIF results file [%s]: %s", o.sarifFilePath, err) | ||||||
| } | ||||||
| if len(o.payload.SarifResults.Results) > 0 { | ||||||
| r := o.payload.SarifResults.Results[0] | ||||||
| logger.Debug("SARIF parsed: tool=%s findings=%d high, %d medium, %d low (compliant=%t)", | ||||||
| o.payload.SarifResults.Tool.Name, r.HighCount, r.MediumCount, r.LowCount, o.payload.Compliant) | ||||||
| } | ||||||
|
Comment on lines
+188
to
+192
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: the } else {
logger.Debug("SARIF parsed: tool=%s, no results found", o.payload.SarifResults.Tool.Name)
}Nice addition of the debug logging overall — it's a good practice for production troubleshooting. |
||||||
|
|
||||||
| if o.uploadResultsFile { | ||||||
| o.attachments = append(o.attachments, o.sarifFilePath) | ||||||
| } | ||||||
|
|
||||||
| form, cleanupNeeded, evidencePath, err := prepareAttestationForm(o.payload, o.attachments) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| // if we created a tar package, remove it after uploading it | ||||||
| if cleanupNeeded { | ||||||
| defer func() { | ||||||
| if err := os.Remove(evidencePath); err != nil { | ||||||
| logger.Warn("failed to remove evidence file %s: %v", evidencePath, err) | ||||||
| } | ||||||
| }() | ||||||
| } | ||||||
|
|
||||||
| reqParams := &requests.RequestParams{ | ||||||
| Method: http.MethodPost, | ||||||
| URL: url, | ||||||
| Form: form, | ||||||
| DryRun: global.DryRun, | ||||||
| Token: global.ApiToken, | ||||||
| } | ||||||
| _, err = kosliClient.Do(reqParams) | ||||||
| if err == nil && !global.DryRun { | ||||||
| logger.Info("sarif attestation '%s' is reported to trail: %s", o.payload.AttestationName, o.trailName) | ||||||
| } | ||||||
| return wrapAttestationError(err) | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/suite" | ||
| ) | ||
|
|
||
| // Define the suite, and absorb the built-in basic suite | ||
| // functionality from testify - including a T() method which | ||
| // returns the current testing context | ||
| type AttestSarifCommandTestSuite struct { | ||
| flowName string | ||
| trailName string | ||
| artifactFingerprint string | ||
| suite.Suite | ||
| defaultKosliArguments string | ||
| } | ||
|
|
||
| func (suite *AttestSarifCommandTestSuite) SetupTest() { | ||
| suite.flowName = "attest-sarif" | ||
| suite.trailName = "test-123" | ||
| suite.artifactFingerprint = "7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9" | ||
| global = &GlobalOpts{ | ||
| ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY", | ||
| Org: "docs-cmd-test-user", | ||
| Host: "http://localhost:8001", | ||
| } | ||
| suite.defaultKosliArguments = fmt.Sprintf(" --flow %s --trail %s --repo-root ../.. --host %s --org %s --api-token %s", suite.flowName, suite.trailName, global.Host, global.Org, global.ApiToken) | ||
| CreateFlowWithTemplate(suite.flowName, "testdata/valid_template.yml", suite.T()) | ||
| BeginTrail(suite.trailName, suite.flowName, "", suite.T()) | ||
| CreateArtifactOnTrail(suite.flowName, suite.trailName, "cli", suite.artifactFingerprint, "file1", suite.T()) | ||
| } | ||
|
|
||
| func (suite *AttestSarifCommandTestSuite) TestAttestSarifCmd() { | ||
| tests := []cmdTestCase{ | ||
|
Comment on lines
+36
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test suite only exercises Snyk SARIF fixtures ( |
||
| { | ||
| wantError: true, | ||
| name: "fails when more arguments are provided", | ||
| cmd: fmt.Sprintf("attest sarif foo bar %s", suite.defaultKosliArguments), | ||
| golden: "Error: accepts at most 1 arg(s), received 2 [foo bar]\n", | ||
| }, | ||
| { | ||
| wantError: true, | ||
| name: "fails when missing required flags", | ||
| cmd: fmt.Sprintf("attest sarif foo -t file %s", suite.defaultKosliArguments), | ||
| golden: "Error: required flag(s) \"name\", \"scan-results\" not set\n", | ||
| }, | ||
| { | ||
| wantError: true, | ||
| name: "fails when both --fingerprint and --artifact-type", | ||
| cmd: fmt.Sprintf("attest sarif testdata/file1 --fingerprint xxxx --artifact-type file --name bar --commit HEAD --origin-url https://example.com %s", suite.defaultKosliArguments), | ||
| golden: "Error: only one of --fingerprint, --artifact-type is allowed\n", | ||
| }, | ||
| { | ||
| wantError: true, | ||
| name: "fails when --fingerprint is not valid", | ||
| cmd: fmt.Sprintf("attest sarif --name foo --fingerprint xxxx --commit HEAD --origin-url https://example.com %s", suite.defaultKosliArguments), | ||
| golden: "Error: xxxx is not a valid SHA256 fingerprint. It should match the pattern ^([a-f0-9]{64})$\nUsage: kosli attest sarif [IMAGE-NAME | FILE-PATH | DIR-PATH] [flags]\n", | ||
| }, | ||
| { | ||
| wantError: true, | ||
| name: "attesting against an artifact that does not exist fails", | ||
| cmd: fmt.Sprintf("attest sarif --fingerprint 1234e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 --name foo --commit HEAD --origin-url https://example.com --scan-results testdata/snyk_sarif.json %s", suite.defaultKosliArguments), | ||
| golden: "Error: Artifact with fingerprint 1234e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 does not exist in trail \"test-123\" of flow \"attest-sarif\" belonging to organization \"docs-cmd-test-user\"\n", | ||
| }, | ||
| { | ||
| wantError: true, | ||
| name: "fails when --scan-results is missing", | ||
| cmd: fmt.Sprintf("attest sarif testdata/file1 --artifact-type file --name foo --commit HEAD --origin-url https://example.com %s", suite.defaultKosliArguments), | ||
| golden: "Error: required flag(s) \"scan-results\" not set\n", | ||
| }, | ||
| { | ||
| name: "can attest sarif against an artifact using artifact name and --artifact-type", | ||
| cmd: fmt.Sprintf("attest sarif testdata/file1 --artifact-type file --name foo --commit HEAD --origin-url https://example.com --scan-results testdata/snyk_sarif.json %s", suite.defaultKosliArguments), | ||
| golden: "sarif attestation 'foo' is reported to trail: test-123\n", | ||
| }, | ||
| { | ||
| name: "can attest sarif against an artifact using artifact name and --artifact-type when --name does not exist in the trail template", | ||
| cmd: fmt.Sprintf("attest sarif testdata/file1 --artifact-type file --name bar --commit HEAD --origin-url https://example.com --scan-results testdata/snyk_sarif.json %s", suite.defaultKosliArguments), | ||
| golden: "sarif attestation 'bar' is reported to trail: test-123\n", | ||
| }, | ||
| { | ||
| name: "can attest sarif against an artifact using --fingerprint", | ||
| cmd: fmt.Sprintf("attest sarif --fingerprint 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 --name foo --commit HEAD --origin-url https://example.com --scan-results testdata/snyk_sarif.json %s", suite.defaultKosliArguments), | ||
| golden: "sarif attestation 'foo' is reported to trail: test-123\n", | ||
| }, | ||
| { | ||
| name: "can attest sarif against a trail", | ||
| cmd: fmt.Sprintf("attest sarif --name bar --commit HEAD --origin-url https://example.com --scan-results testdata/snyk_sarif.json %s", suite.defaultKosliArguments), | ||
| golden: "sarif attestation 'bar' is reported to trail: test-123\n", | ||
| }, | ||
| { | ||
| name: "can attest sarif against a trail with --compliant=false", | ||
| cmd: fmt.Sprintf("attest sarif --name bar --commit HEAD --origin-url https://example.com --scan-results testdata/snyk_sarif.json --compliant=false %s", suite.defaultKosliArguments), | ||
| golden: "sarif attestation 'bar' is reported to trail: test-123\n", | ||
| }, | ||
| { | ||
| name: "can attest sarif against a trail with explicit --compliant=true", | ||
| cmd: fmt.Sprintf("attest sarif --name bar --commit HEAD --origin-url https://example.com --scan-results testdata/snyk_sarif.json --compliant=true %s", suite.defaultKosliArguments), | ||
| golden: "sarif attestation 'bar' is reported to trail: test-123\n", | ||
| }, | ||
| { | ||
| name: "can attest sarif against a trail when name is not found in the trail template", | ||
| cmd: fmt.Sprintf("attest sarif --name additional --commit HEAD --origin-url https://example.com --scan-results testdata/snyk_sarif.json %s", suite.defaultKosliArguments), | ||
| golden: "sarif attestation 'additional' is reported to trail: test-123\n", | ||
| }, | ||
| { | ||
| name: "can attest sarif against an artifact created using dot syntax in --name", | ||
| cmd: fmt.Sprintf("attest sarif --name cli.foo --commit HEAD --origin-url https://example.com --scan-results testdata/snyk_sarif.json %s", suite.defaultKosliArguments), | ||
| golden: "sarif attestation 'foo' is reported to trail: test-123\n", | ||
| }, | ||
| { | ||
| name: "can attest sarif with external-url and external-fingerprint against a trail", | ||
| cmd: fmt.Sprintf(`attest sarif --name bar --commit HEAD --origin-url https://example.com | ||
| --external-url file=https://example.com/file --external-url other=https://other.com | ||
| --external-fingerprint file=7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 | ||
| --scan-results testdata/snyk_sarif.json %s`, suite.defaultKosliArguments), | ||
| golden: "sarif attestation 'bar' is reported to trail: test-123\n", | ||
| }, | ||
| { | ||
| name: "can attest sarif with annotations against a trail", | ||
| cmd: fmt.Sprintf(`attest sarif --name bar --commit HEAD --origin-url https://example.com | ||
| --annotate foo=bar --annotate baz=qux | ||
| --scan-results testdata/snyk_sarif.json %s`, suite.defaultKosliArguments), | ||
| golden: "sarif attestation 'bar' is reported to trail: test-123\n", | ||
| }, | ||
| { | ||
| wantError: true, | ||
| name: "fails when annotation is not valid", | ||
| cmd: fmt.Sprintf(`attest sarif --name bar --commit HEAD --origin-url https://example.com | ||
| --annotate foo.baz=bar | ||
| --scan-results testdata/snyk_sarif.json %s`, suite.defaultKosliArguments), | ||
| golden: "Error: --annotate flag should be in the format key=value. Invalid key: 'foo.baz'. Key can only contain [A-Za-z0-9_]\n", | ||
| }, | ||
| { | ||
| wantError: true, | ||
| name: "fails when --name has invalid dot format", | ||
| cmd: fmt.Sprintf("attest sarif --name .foo --scan-results testdata/snyk_sarif.json %s", suite.defaultKosliArguments), | ||
| golden: "Error: failed to parse attestation name: invalid attestation name format: .foo\n", | ||
| }, | ||
| } | ||
|
|
||
| runTestCmd(suite.T(), tests) | ||
| } | ||
|
|
||
| // In order for 'go test' to run this suite, we need to create | ||
| // a normal test function and pass our suite to suite.Run | ||
| func TestAttestSarifCommandTestSuite(t *testing.T) { | ||
| suite.Run(t, new(AttestSarifCommandTestSuite)) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Trailing whitespace in the short description string (two spaces before the closing backtick). This is consistent with the existing
attestSnykShortDesc, but if you're cleaning things up it's worth removing.