Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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"`
Comment thread
AlexKantor87 marked this conversation as resolved.
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. `
Comment thread
AlexKantor87 marked this conversation as resolved.
Outdated
Comment thread
AlexKantor87 marked this conversation as resolved.
Outdated

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 thread
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)
}
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 thread
AlexKantor87 marked this conversation as resolved.
Comment thread
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))
}
Loading
Loading