Skip to content
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: allow findings to be ignored #25

Merged
merged 26 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a80607b
ci: configure exhaustive linter
jamestelfer Nov 20, 2023
d0f2fc1
feat: add ability to parse an ignore file
jamestelfer Nov 20, 2023
23f4a92
feat: load a series of ignore files from disk
jamestelfer Nov 21, 2023
2f85239
feat: ignored findings affect threshold calculations
jamestelfer Nov 21, 2023
1784832
fix: use summarized results in report annotation
jamestelfer Nov 21, 2023
7b13cc1
fix: annotate ignored findings in report
jamestelfer Nov 21, 2023
c26c6d5
fix: transition to view objects for report
jamestelfer Nov 21, 2023
9d3e184
feat: render ignored findings in separate table
jamestelfer Nov 21, 2023
088276c
fix: include finding description in annotation
jamestelfer Nov 22, 2023
2d076a4
fix: simplify ignore parsing by only accepting map
jamestelfer Nov 22, 2023
8e83832
docs: typo in README
jamestelfer Nov 22, 2023
c9d25e9
fix: filtered expired finding config entries
jamestelfer Nov 23, 2023
3f47c32
fix: correct casing of URI to align with linter
jamestelfer Nov 22, 2023
0f08784
fix: avoid markdown rendering in table cells
jamestelfer Nov 22, 2023
b934c56
docs: add docs for ignore file specification
jamestelfer Nov 23, 2023
1b11ee5
docs: comment Markdown rendering quirk
jamestelfer Nov 23, 2023
748b1fe
docs: increase vulnerability handling documentation
jamestelfer Nov 23, 2023
8b8df74
fix: use /etc directory instead of home
jamestelfer Nov 26, 2023
4aab487
fix: collapse logic when matching ignore to finding
jamestelfer Nov 27, 2023
120cc52
fix: use type alias for severity
jamestelfer Nov 27, 2023
638d854
fix: make summary constructor private
jamestelfer Nov 27, 2023
32b0cf5
fix: avoid the unnecessary default to empty
jamestelfer Nov 27, 2023
bd04f4d
doc: removed unhelpful doc comments
jamestelfer Nov 27, 2023
e36fa45
refactor: split loadignores function
jamestelfer Nov 27, 2023
c90c99e
refactor: inline temp vars in filter function
jamestelfer Nov 27, 2023
5984d6c
Merge branch 'main' into add-ignore-file
jamestelfer Nov 28, 2023
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
7 changes: 7 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ linters:
- revive
- tagalign
- whitespace
- musttag
# deprecated linters
- interfacer
- golint
Expand All @@ -62,5 +63,11 @@ linters-settings:
audit: disabled
G101: # "Look for hard-coded credentials"
mode: strict

cyclop:
max-complexity: 20

exhaustive:
# Presence of "default" case in switch statements satisfies exhaustiveness,
# even if all enum members are not listed.
default-signifies-exhaustive: true
93 changes: 76 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ service. By default the plugin will cause the step to fail if there are critical
or high vulnerabilities reported, but there are configurable thresholds on this
behaviour.

> ℹ️ TIP: if you want the build to continue when vulnerabilities are found, be
> ℹ️ **TIP**: if you want the build to continue when vulnerabilities are found, be
> sure to supply values for `max-criticals` and `max-highs` parameters. If these
> are set to high values your build will never fail, but details will be
> supplied in the annotation.
>
> Check out the FAQs below for more information
> If a finding is irrelevant, or you're waiting on an upstream fix, use an
> "ignore" configuration file instead: see the [ignore
> findings](./docs/ignore-findings.md) documentation.

## Example

Expand Down Expand Up @@ -76,12 +78,20 @@ If the number of critical vulnerabilities in the image exceeds this threshold
the build is failed. Defaults to 0. Use a sufficiently large number (e.g. 999)
to allow the build to always pass.

> [!IMPORTANT]
> Prefer an [ignore file](./docs/ignore-findings.md) over setting thresholds if
> a finding is irrelevant or time to respond is required.

### `max-highs` (Optional, string)

If the number of high vulnerabilities in the image exceeds this threshold the
build is failed. Defaults to 0. Use a sufficiently large number (e.g. 999) to
allow the build to always pass.

> [!IMPORTANT]
> Prefer an [ignore file](./docs/ignore-findings.md) over setting thresholds if
> a finding is irrelevant or time to respond is required.

### `image-label` (Optional, string)

When supplied, this is used to title the report annotation in place of the
Expand Down Expand Up @@ -118,18 +128,67 @@ for more information.

## FAQ

### I have a vulnerability that isn't resolved yet, but I can wait on fixing. How do I do configure this plugin so I can unblock my builds?

Refer to how to set your [max-criticals](https://github.com/cultureamp/ecr-scan-results-buildkite-plugin#max-criticals-optional-string), and [max-highs](https://github.com/cultureamp/ecr-scan-results-buildkite-plugin#max-highs-optional-string).

### Are there guidelines on using up?

Yes. Changing the `max-criticals` and `max-high` settings should not be taken lightly.

This option is effectively a deferral of fixing the vulnerability. **Assess the situation first**. If the CVE describes a scenario that aligns with how your project is used, then you should be working to fix it rather than defer it. For help on this, check out the following the steps outlined [here](https://cultureamp.atlassian.net/wiki/spaces/PST/pages/2960916852/Central+SRE+Support+FAQs#I-have-high%2Fcritical-vulnerabilities-for-my-ECR-image%2C-and-its-blocking-my-builds.-What%E2%80%99s-going-on%3F).

Below are some recommendations if you choose to exercise this option:

1. Set the thresholds to the number of identified high or critical vulnerabilities. This is so you’re not permitting more vulnerabilities than you should. Especially for those you can fix by updating dependencies or packages.

2. Set a scheduled reminder for your team to check if a fix is available for the CVE. If a fix is available, address it, and then lower your threshold for the respective vulnerability severity.
### The build is failing for an unresolved vulnerability. How do I do configure this plugin so I can unblock my builds?

There are two options here:

1. configure an [ignore file](docs/ignore-findings.md) to tell the plugin to
skip this vulnerability. Set an `until` date to ensure that it is dealt with
in the future.
2. refer to how to set your [max-criticals](#max-criticals-optional-string), and
[max-highs](#max-highs-optional-string) thresholds.

Out of the two options, the first is far more preferable as it targets the
finding that is causing the problem. Altering a threshold is unlikely to be
rolled back and is indiscriminate about the findings that it will ignore.

### How do I set my thresholds? _OR_ My build often breaks because of this plugin. Should I change the thresholds?

Setting the `max-criticals` and `max-high` thresholds requires some thought, and
needs to take into account the risk profile of the company and the service
itself.

Allowing a container with a critical or high vulnerability to be deployed is not
necessarily wrong, it really depends on the environment of the deployed
application and the nature of the vulnerabilities themselves.

Consult with your company's internal security teams about the acceptable risk
level for a service if a non-zero threshold is desired, and consider making use
of the [ignore file](docs/ignore-findings.md) configuration to avoid temporary
blockages. Using an ignore entry with an expiry is strongly recommended over
increasing the threshold.

### What should I think about when ignoring a finding for a period of time?

Consider the following:

- Follow the link to read the details of the CVE. Does it impact a component
that is directly (or indirectly) used by your application?
- Reference the CVSS score associated with the vulnerability and look carefully
at the vector via the link. The vector will help inform about how this
vulnerability can affect your system.
- Consider using the CVSS calculator (reached via the vector link) to fill out
the "Environmental" part of the score. The environmental score helps judge the
risks posed in the context of your application deployment environment. This is
likely adjust the base score and assist in your decision making.
- Is a patch or update already available?
- Is this update likely to be made available in an update to the current base
image, or does it exist in a later version of the base image?

Possible actions:

1. Update the base image that incorporates the fix. This may be the simplest
option.
2. Remove the dependency from the image, or choose a different base image
without the issue.
3. Add an ignore with an expiry to allow for an update to be published within a
period of time. This is only valid if your corporate standards allow it, and
there is a plan to follow up on this issue in the given time frame.
4. Update the image definition to update the package with the vulnerability.
This can negatively impact image size, and create a future maintenance issue
if not done carefully. Take care to keep the number of packages updated
small, and avoid adding hard-coded versions unless a process exists to keep
them up-to-date.
5. Ignore the finding indefinitely. This will generally only be valid if
dependency both cannot be removed and is not used in a vulnerable fashion.
Your working environment may require special exceptions for this.
44 changes: 44 additions & 0 deletions docs/ignore-findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Ignoring findings

Findings can be ignored using a YAML file with the following structure:

```yaml
ignores:
- id: CVE-2023-100
- id: CVE-2023-200
until: 2023-12-31
reason: allowing 2 weeks for base image to update
- id: CVE-2023-300
```

- each element must have at least the `id` field
- the `until` field defines the expiry of this ignore entry. This allows a team time to respond while temporarily allowing builds to continue.
- the `reason` field gives a justification that is rendered in the annotation for greater visibility. Including the "why" in this field is highly recommended.

Ignore configuration can be specified in a number of places. If a listing for a finding with the same CVE name appears in multiple files, the most local wins: central configuration can be overridden by the repository.

From least to most important:

- `/etc/ecr-scan-results-buildkite-plugin/ignore.y[a]ml` (part of the agent, not modifiable by builds)
- `buildkite/ecr-scan-results-ignore.y[a]ml` (specified alongside the pipeline)
- `.buildkite/ecr-scan-results-ignore.y[a]ml`
- `.ecr-scan-results-ignore.y[a]ml` (local repository configuration)

Configuration in the `/etc/ecr-scan-results-buildkite-plugin` directory allows for organizations to ship agents with plugin configuration that centrally manages findings that can be ignored.

> [!IMPORTANT]
> When a finding is ignored, it is removed from consideration for threshold checks, but it's not discarded. The annotation created by the plugin adds details to the results, giving high visibility on the configured behaviour.

## Rendering

The summary counts at the top show the number of ignored findings:

<img src="img/summary-counts.png" alt="summary counts" width="60%">

Ignored findings are separated from the main list and shown at the bottom:

<img src="img/ignore-finding-list.png" alt="ignored finding list" width="60%">

If a reason for ignoring a finding is provided, it's made available by expanding the Until date:

<img src="img/ignore-reason.png" alt="ignored reason" width="60%">
Binary file added docs/img/ignore-finding-list.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/ignore-reason.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/summary-counts.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
139 changes: 139 additions & 0 deletions src/finding/summary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package finding

import (
"slices"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/cultureamp/ecrscanresults/findingconfig"
)

type Detail struct {
// The name associated with the finding, usually a CVE number.
Name string

URI string

// The description of the finding.
Description string

// The finding severity.
Severity types.FindingSeverity
jamestelfer marked this conversation as resolved.
Show resolved Hide resolved

PackageName string
PackageVersion string
CVSS2Score string
CVSS2Vector string

Ignore *findingconfig.Ignore
}

type SeverityCount struct {
// Included is the number of findings that count towards the threshold for this severity.
Included int32

// Ignored is the number of findings that were ignored for the purposes of the threshold.
Ignored int32
}

type Summary struct {
// the counts by threshold, taking ignore configuration into account
Counts map[types.FindingSeverity]SeverityCount

Details []Detail

// the set of finding IDs that have been ignored by configuration
Ignored []Detail

// The time of the last completed image scan.
ImageScanCompletedAt *time.Time

// The time when the vulnerability data was last scanned.
VulnerabilitySourceUpdatedAt *time.Time
}

func (s *Summary) addDetail(d Detail) {
s.Details = append(s.Details, d)
s.updateCount(d.Severity, SeverityCount{Included: 1})
}

func (s *Summary) addIgnored(d Detail) {
s.Ignored = append(s.Ignored, d)
s.updateCount(d.Severity, SeverityCount{Ignored: 1})
}

func (s *Summary) updateCount(severity types.FindingSeverity, updateBy SeverityCount) {
counts := SeverityCount{}
if c, exists := s.Counts[severity]; exists {
counts = c
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: usually, this logic is inverted

Suggested change
counts := SeverityCount{}
if c, exists := s.Counts[severity]; exists {
counts = c
}
c := SeverityCount{}
if counts, exists := s.Counts[severity]; !exists {
counts = c
}

Copy link
Member Author

Choose a reason for hiding this comment

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

The variables defined as a part of the assignment in the if clause (if counts, exists := ) are only in scope in the body of the if, so inverting as suggested won't compile.

The error is counts declared and not used.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've changed this to:

counts := s.Counts[severity]

Since the default value is returned if the key is not in the map, which is what I'm after.


counts.Ignored += updateBy.Ignored
counts.Included += updateBy.Included

s.Counts[severity] = counts
}

func NewSummary() Summary {
jamestelfer marked this conversation as resolved.
Show resolved Hide resolved
return Summary{
Counts: map[types.FindingSeverity]SeverityCount{
"CRITICAL": {},
"HIGH": {},
},
Details: []Detail{},
Ignored: []Detail{},
}
}

func Summarize(findings *types.ImageScanFindings, ignoreConfig []findingconfig.Ignore) Summary {
summary := NewSummary()

summary.ImageScanCompletedAt = findings.ImageScanCompletedAt
summary.VulnerabilitySourceUpdatedAt = findings.VulnerabilitySourceUpdatedAt

for _, f := range findings.Findings {
var ignore *findingconfig.Ignore
detail := findingToDetail(f)

index := slices.IndexFunc(ignoreConfig, func(ignore findingconfig.Ignore) bool {
return ignore.ID == detail.Name
})
ctgardner marked this conversation as resolved.
Show resolved Hide resolved

if index >= 0 {
ignore = &ignoreConfig[index]
}

detail.Ignore = ignore

if ignore == nil {
summary.addDetail(detail)
} else {
summary.addIgnored(detail)
}
jamestelfer marked this conversation as resolved.
Show resolved Hide resolved
}

return summary
}

func findingToDetail(finding types.ImageScanFinding) Detail {
return Detail{
Name: aws.ToString(finding.Name),
URI: aws.ToString(finding.Uri),
Description: aws.ToString(finding.Description),
Severity: finding.Severity,
PackageName: findingAttributeValue(finding, "package_name"),
PackageVersion: findingAttributeValue(finding, "package_version"),
CVSS2Score: findingAttributeValue(finding, "CVSS2_SCORE"),
CVSS2Vector: findingAttributeValue(finding, "CVSS2_VECTOR"),
}
}

func findingAttributeValue(finding types.ImageScanFinding, name string) string {
for _, a := range finding.Attributes {
if aws.ToString(a.Key) == name {
return aws.ToString(a.Value)
}
}
return ""
}
Loading