-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
feat(report): add support for Cosign vulnerability attestation #2567
Changes from 14 commits
483ca4e
f3c8f57
106599d
73c500f
3e5a691
e1c3d5b
ce515d3
f9cb8a2
cdd986d
cda3cb5
8a63658
fa1b583
cbff270
6dad54a
0add976
b02e159
1e37c6d
c443551
ab9bcaf
7aaa34c
f50124a
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,167 @@ | ||
# Cosign Vulnerability Attestation | ||
|
||
## Generate Cosign Vulnerability Predicate | ||
|
||
Trivy generates reports in the [Cosign vulnerability predicate format](https://github.com/sigstore/cosign/blob/95b74db89941e8ec85e768f639efd4d948db06cd/specs/COSIGN_VULN_ATTESTATION_SPEC.md). | ||
|
||
You can use the regular subcommands (like image, fs and rootfs) and specify `cosign-vuln` with the --format option. | ||
|
||
``` | ||
$ trivy image --format cosign-vuln --output vuln.json alpine:3.10 | ||
``` | ||
|
||
<details> | ||
<summary>Result</summary> | ||
|
||
```json | ||
{ | ||
"invocation": { | ||
"parameters": null, | ||
"uri": "", | ||
"event_id": "", | ||
"builder.id": "" | ||
}, | ||
"scanner": { | ||
"uri": "pkg:github/aquasecurity/trivy@v0.30.1-8-gf9cb8a28", | ||
"version": "v0.30.1-8-gf9cb8a28", | ||
"db": { | ||
"uri": "", | ||
"version": "" | ||
}, | ||
"result": { | ||
"SchemaVersion": 2, | ||
"ArtifactName": "alpine:3.10", | ||
"ArtifactType": "container_image", | ||
"Metadata": { | ||
"OS": { | ||
"Family": "alpine", | ||
"Name": "3.10.9", | ||
"EOSL": true | ||
}, | ||
"ImageID": "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a", | ||
"DiffIDs": [ | ||
"sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635" | ||
], | ||
"RepoTags": [ | ||
"alpine:3.10" | ||
], | ||
"RepoDigests": [ | ||
"alpine@sha256:451eee8bedcb2f029756dc3e9d73bab0e7943c1ac55cff3a4861c52a0fdd3e98" | ||
], | ||
"ImageConfig": { | ||
"architecture": "amd64", | ||
"container": "fdb7e80e3339e8d0599282e606c907aa5881ee4c668a68136119e6dfac6ce3a4", | ||
"created": "2021-04-14T19:20:05.338397761Z", | ||
"docker_version": "19.03.12", | ||
"history": [ | ||
{ | ||
"created": "2021-04-14T19:20:04.987219124Z", | ||
"created_by": "/bin/sh -c #(nop) ADD file:c5377eaa926bf412dd8d4a08b0a1f2399cfd708743533b0aa03b53d14cb4bb4e in / " | ||
}, | ||
{ | ||
"created": "2021-04-14T19:20:05.338397761Z", | ||
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", | ||
"empty_layer": true | ||
} | ||
], | ||
"os": "linux", | ||
"rootfs": { | ||
"type": "layers", | ||
"diff_ids": [ | ||
"sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635" | ||
] | ||
}, | ||
"config": { | ||
"Cmd": [ | ||
"/bin/sh" | ||
], | ||
"Env": [ | ||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" | ||
], | ||
"Image": "sha256:eb2080c455e94c22ae35b3aef9e078c492a00795412e026e4d6b41ef64bc7dd8" | ||
} | ||
} | ||
}, | ||
"Results": [ | ||
{ | ||
"Target": "alpine:3.10 (alpine 3.10.9)", | ||
"Class": "os-pkgs", | ||
"Type": "alpine", | ||
"Vulnerabilities": [ | ||
{ | ||
"VulnerabilityID": "CVE-2021-36159", | ||
"PkgName": "apk-tools", | ||
"InstalledVersion": "2.10.6-r0", | ||
"FixedVersion": "2.10.7-r0", | ||
"Layer": { | ||
"Digest": "sha256:396c31837116ac290458afcb928f68b6cc1c7bdd6963fc72f52f365a2a89c1b5", | ||
"DiffID": "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635" | ||
}, | ||
"SeveritySource": "nvd", | ||
"PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-36159", | ||
"DataSource": { | ||
"ID": "alpine", | ||
"Name": "Alpine Secdb", | ||
"URL": "https://secdb.alpinelinux.org/" | ||
}, | ||
"Description": "libfetch before 2021-07-26, as used in apk-tools, xbps, and other products, mishandles numeric strings for the FTP and HTTP protocols. The FTP passive mode implementation allows an out-of-bounds read because strtol is used to parse the relevant numbers into address bytes. It does not check if the line ends prematurely. If it does, the for-loop condition checks for the '\\0' terminator one byte too late.", | ||
"Severity": "CRITICAL", | ||
"CweIDs": [ | ||
"CWE-125" | ||
], | ||
"CVSS": { | ||
"nvd": { | ||
"V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:P", | ||
"V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H", | ||
"V2Score": 6.4, | ||
"V3Score": 9.1 | ||
} | ||
}, | ||
"References": [ | ||
"https://github.com/freebsd/freebsd-src/commits/main/lib/libfetch", | ||
"https://gitlab.alpinelinux.org/alpine/apk-tools/-/issues/10749", | ||
"https://lists.apache.org/thread.html/r61db8e7dcb56dc000a5387a88f7a473bacec5ee01b9ff3f55308aacc@%3Cdev.kafka.apache.org%3E", | ||
"https://lists.apache.org/thread.html/r61db8e7dcb56dc000a5387a88f7a473bacec5ee01b9ff3f55308aacc@%3Cusers.kafka.apache.org%3E", | ||
"https://lists.apache.org/thread.html/rbf4ce74b0d1fa9810dec50ba3ace0caeea677af7c27a97111c06ccb7@%3Cdev.kafka.apache.org%3E", | ||
"https://lists.apache.org/thread.html/rbf4ce74b0d1fa9810dec50ba3ace0caeea677af7c27a97111c06ccb7@%3Cusers.kafka.apache.org%3E" | ||
], | ||
"PublishedDate": "2021-08-03T14:15:00Z", | ||
"LastModifiedDate": "2021-10-18T12:19:00Z" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
}, | ||
"metadata": { | ||
"scanStartedOn": "2022-07-24T17:14:04.864682+09:00", | ||
"scanFinishedOn": "2022-07-24T17:14:04.864682+09:00" | ||
} | ||
} | ||
``` | ||
|
||
</details> | ||
|
||
## Create Cosign Vulnerability Attestation | ||
|
||
[Cosign](https://github.com/sigstore/cosign) supports generating and verifying [in-toto attestations](https://github.com/in-toto/attestation). This tool enables you to sign and verify Cosign vulnerability attestation. | ||
|
||
!!! note | ||
In the following examples, the `cosign` command will write an attestation to a target OCI registry, so you must have permission to write. | ||
If you want to avoid writing an OCI registry and only want to see an attestation, add the `--no-upload` option to the `cosign` command. | ||
|
||
|
||
Cosign can generate key pairs and use them for signing and verification. Read more about [how to generate key pairs](https://docs.sigstore.dev/cosign/key-generation). | ||
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. Can we add a keyless section? 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. Alright. I've added a keyless signing section. |
||
|
||
In the following example, Trivy generates a cosign vulnerability predicate, and then Cosign attaches an attestation of it to a container image with a local key pair. | ||
|
||
``` | ||
$ trivy image --format cosign-vuln --output vuln.json <IMAGE> | ||
$ cosign attest --key /path/to/cosign.key --type vuln --predicate vuln.json <IMAGE> | ||
``` | ||
|
||
Then, you can verify attestations on the image. | ||
|
||
``` | ||
$ cosign verify-attestation --key /path/to/cosign.pub <IMAGE> | ||
``` |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -74,6 +74,7 @@ nav: | |||||
- SPDX: docs/sbom/spdx.md | ||||||
- Attestation: | ||||||
- SBOM: docs/attestation/sbom.md | ||||||
- Cosign Vulnerability Predicate: docs/attestation/vuln.md | ||||||
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. To align with their doc.
https://github.com/sigstore/cosign/blob/main/specs/COSIGN_VULN_ATTESTATION_SPEC.md
Suggested change
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. Done |
||||||
- Integrations: | ||||||
- Overview: docs/integrations/index.md | ||||||
- GitHub Actions: docs/integrations/github-actions.md | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,89 @@ | ||||||
package predicate | ||||||
|
||||||
import ( | ||||||
"encoding/json" | ||||||
"fmt" | ||||||
"io" | ||||||
"time" | ||||||
|
||||||
"github.com/package-url/packageurl-go" | ||||||
"golang.org/x/xerrors" | ||||||
|
||||||
"github.com/aquasecurity/trivy/pkg/clock" | ||||||
"github.com/aquasecurity/trivy/pkg/types" | ||||||
) | ||||||
|
||||||
// CosignVulnPredicate represents the Cosign Vulnerability predicate. | ||||||
// Cosign provides the CosignVulnPredicate structure in their repository. | ||||||
// But the type of Scanner.Result is defined as map[string]interface{}, which is difficult to use, | ||||||
// so we define our own. | ||||||
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. Add this link, please. 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. Alright. I've added the link. |
||||||
type CosignVulnPredicate struct { | ||||||
Invocation Invocation `json:"invocation"` | ||||||
Scanner Scanner `json:"scanner"` | ||||||
Metadata Metadata `json:"metadata"` | ||||||
} | ||||||
|
||||||
type Invocation struct { | ||||||
Parameters interface{} `json:"parameters"` | ||||||
URI string `json:"uri"` | ||||||
EventID string `json:"event_id"` | ||||||
BuilderID string `json:"builder.id"` | ||||||
} | ||||||
|
||||||
type DB struct { | ||||||
URI string `json:"uri"` | ||||||
Version string `json:"version"` | ||||||
} | ||||||
|
||||||
type Scanner struct { | ||||||
URI string `json:"uri"` | ||||||
Version string `json:"version"` | ||||||
DB DB `json:"db"` | ||||||
Result types.Report `json:"result"` | ||||||
} | ||||||
|
||||||
type Metadata struct { | ||||||
ScanStartedOn time.Time `json:"scanStartedOn"` | ||||||
ScanFinishedOn time.Time `json:"scanFinishedOn"` | ||||||
} | ||||||
|
||||||
type Writer struct { | ||||||
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. We may have more predicates in the future.
Suggested change
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. Done |
||||||
output io.Writer | ||||||
version string | ||||||
} | ||||||
|
||||||
func NewWriter(output io.Writer, version string) Writer { | ||||||
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.
Suggested change
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. Done |
||||||
return Writer{ | ||||||
output: output, | ||||||
version: version, | ||||||
} | ||||||
} | ||||||
|
||||||
func (w Writer) Write(report types.Report) error { | ||||||
|
||||||
predicate := CosignVulnPredicate{} | ||||||
|
||||||
purl := packageurl.NewPackageURL("github", "aquasecurity", "trivy", w.version, nil, "") | ||||||
predicate.Scanner = Scanner{ | ||||||
URI: purl.ToString(), | ||||||
Version: w.version, | ||||||
Result: report, | ||||||
} | ||||||
|
||||||
now := clock.Now() | ||||||
predicate.Metadata = Metadata{ | ||||||
ScanStartedOn: now, | ||||||
ScanFinishedOn: now, | ||||||
} | ||||||
|
||||||
output, err := json.MarshalIndent(predicate, "", " ") | ||||||
if err != nil { | ||||||
return xerrors.Errorf("failed to marshal cosign vulnerability predicate: %w", err) | ||||||
} | ||||||
|
||||||
if _, err = fmt.Fprint(w.output, string(output)); err != nil { | ||||||
return xerrors.Errorf("failed to write cosign vulnerability predicate: %w", err) | ||||||
} | ||||||
return nil | ||||||
|
||||||
} |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,113 @@ | ||||||||
package predicate_test | ||||||||
|
||||||||
import ( | ||||||||
"bytes" | ||||||||
"encoding/json" | ||||||||
"testing" | ||||||||
"time" | ||||||||
|
||||||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types" | ||||||||
"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" | ||||||||
"github.com/stretchr/testify/assert" | ||||||||
|
||||||||
"github.com/aquasecurity/trivy/pkg/clock" | ||||||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" | ||||||||
"github.com/aquasecurity/trivy/pkg/report/predicate" | ||||||||
"github.com/aquasecurity/trivy/pkg/types" | ||||||||
) | ||||||||
|
||||||||
func TestWriter_Write(t *testing.T) { | ||||||||
testCases := []struct { | ||||||||
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. It is a nitpick, but we're recently trying to comply with the same convention. trivy/pkg/licensing/classifier_test.go Line 16 in d93a997
Suggested change
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. I've fixed it. |
||||||||
name string | ||||||||
detectedVulns []types.DetectedVulnerability | ||||||||
want predicate.CosignVulnPredicate | ||||||||
wantResult types.Report | ||||||||
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. Do we need it? 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. Sorry, I forgot to remove it. |
||||||||
}{ | ||||||||
{ | ||||||||
name: "happy path", | ||||||||
detectedVulns: []types.DetectedVulnerability{ | ||||||||
{ | ||||||||
VulnerabilityID: "CVE-2020-0001", | ||||||||
PkgName: "foo", | ||||||||
InstalledVersion: "1.2.3", | ||||||||
FixedVersion: "3.4.5", | ||||||||
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", | ||||||||
Vulnerability: dbTypes.Vulnerability{ | ||||||||
Title: "foobar", | ||||||||
Description: "baz", | ||||||||
Severity: "HIGH", | ||||||||
VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ | ||||||||
vulnerability.NVD: dbTypes.SeverityHigh, | ||||||||
}, | ||||||||
}, | ||||||||
}, | ||||||||
}, | ||||||||
want: predicate.CosignVulnPredicate{ | ||||||||
Scanner: predicate.Scanner{ | ||||||||
URI: "pkg:github/aquasecurity/trivy@dev", | ||||||||
Version: "dev", | ||||||||
Result: types.Report{ | ||||||||
SchemaVersion: 2, | ||||||||
ArtifactName: "alpine:3.14", | ||||||||
ArtifactType: ftypes.ArtifactType(""), | ||||||||
Metadata: types.Metadata{}, | ||||||||
Results: types.Results{ | ||||||||
{ | ||||||||
Target: "foojson", | ||||||||
Vulnerabilities: []types.DetectedVulnerability{ | ||||||||
{ | ||||||||
VulnerabilityID: "CVE-2020-0001", | ||||||||
PkgName: "foo", | ||||||||
InstalledVersion: "1.2.3", | ||||||||
FixedVersion: "3.4.5", | ||||||||
PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", | ||||||||
Vulnerability: dbTypes.Vulnerability{ | ||||||||
Title: "foobar", | ||||||||
Description: "baz", | ||||||||
Severity: "HIGH", | ||||||||
}, | ||||||||
}, | ||||||||
}, | ||||||||
}, | ||||||||
}, | ||||||||
}, | ||||||||
}, | ||||||||
Metadata: predicate.Metadata{ | ||||||||
ScanStartedOn: time.Date(2022, time.July, 22, 12, 20, 30, 5, time.UTC), | ||||||||
ScanFinishedOn: time.Date(2022, time.July, 22, 12, 20, 30, 5, time.UTC), | ||||||||
}, | ||||||||
}, | ||||||||
}, | ||||||||
} | ||||||||
|
||||||||
for _, tc := range testCases { | ||||||||
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: ditto
Suggested change
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. I've fixed it. |
||||||||
t.Run(tc.name, func(t *testing.T) { | ||||||||
|
||||||||
inputResults := types.Report{ | ||||||||
SchemaVersion: 2, | ||||||||
ArtifactName: "alpine:3.14", | ||||||||
Results: types.Results{ | ||||||||
{ | ||||||||
Target: "foojson", | ||||||||
Vulnerabilities: tc.detectedVulns, | ||||||||
}, | ||||||||
}, | ||||||||
} | ||||||||
|
||||||||
output := bytes.NewBuffer(nil) | ||||||||
|
||||||||
clock.SetFakeTime(t, time.Date(2022, 7, 22, 12, 20, 30, 5, time.UTC)) | ||||||||
writer := predicate.NewWriter(output, "dev") | ||||||||
|
||||||||
err := writer.Write(inputResults) | ||||||||
assert.NoError(t, err) | ||||||||
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: IMHO,
Suggested change
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. It makes sense. I've fixed it. |
||||||||
|
||||||||
var got predicate.CosignVulnPredicate | ||||||||
err = json.Unmarshal(output.Bytes(), &got) | ||||||||
assert.NoError(t, err, "invalid json written") | ||||||||
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. ditto 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. I've fixed it. |
||||||||
|
||||||||
assert.Equal(t, tc.want, got, tc.name) | ||||||||
|
||||||||
}) | ||||||||
} | ||||||||
} |
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.
And put it at the bottom.
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.
I've fixed it.