Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/kosli/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func newAttestCmd(out io.Writer) *cobra.Command {
newAttestArtifactCmd(out),
newAttestGenericCmd(out),
newAttestSnykCmd(out),
newAttestSarifCmd(out),
newAttestJunitCmd(out),
newAttestJiraCmd(out),
newAttestPRCmd(out),
Expand Down
223 changes: 223 additions & 0 deletions cmd/kosli/attestSarif.go
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. `
Copy link
Copy Markdown
Contributor

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.

Suggested change
const attestSarifShortDesc = `Report a SARIF attestation to an artifact or a trail in a Kosli flow. `
const attestSarifShortDesc = `Report a SARIF attestation to an artifact or a trail in a Kosli flow.`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 attestSarifShortDesc = `Report a SARIF attestation to an artifact or a trail in a Kosli flow. `
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)
}
Comment on lines +188 to +192
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the len(o.payload.SarifResults.Results) > 0 guard protects the debug log, but if a valid SARIF file has zero runs (e.g. an empty scan), this silently skips without any debug output. Consider adding an else branch:

} 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)
}
152 changes: 152 additions & 0 deletions cmd/kosli/attestSarif_test.go
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite only exercises Snyk SARIF fixtures (testdata/snyk_sarif.json). Since the PR markets this command as accepting "any compatible SARIF scanner", consider adding at least one test case using a non-Snyk fixture (Checkov or Trivy output) to prove the parser handles different tool.driver.name values and different level formats end-to-end. The existing internal/sarif/sarif_test.go covers the parser, but a command-level integration test would close the loop.

{
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))
}
Loading
Loading