-
Notifications
You must be signed in to change notification settings - Fork 7
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
Draft
AlexKantor87
wants to merge
4
commits into
main
Choose a base branch
from
feat/sarif-attestation-type
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
041aad5
refactor: rename internal/snyk to internal/sarif
AlexKantor87 59a38ff
feat(cli): add `kosli attest sarif` command
AlexKantor87 22b6a5a
chore(cli): add debug logging around SARIF parse step
AlexKantor87 fbfc6e1
chore(cli): address PR review nits on attest sarif
AlexKantor87 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| 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.` | ||
|
|
||
| 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) | ||
| } else { | ||
| logger.Debug("SARIF parsed: tool=%s no scan runs in file (compliant=%t)", | ||
| o.payload.SarifResults.Tool.Name, o.payload.Compliant) | ||
| } | ||
|
AlexKantor87 marked this conversation as resolved.
|
||
|
|
||
| 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) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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{ | ||
|
AlexKantor87 marked this conversation as resolved.
AlexKantor87 marked this conversation as resolved.
|
||
| { | ||
| 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)) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.