Skip to content

Commit 254e348

Browse files
committed
severity filter implementation
1 parent e70cef3 commit 254e348

File tree

6 files changed

+266
-0
lines changed

6 files changed

+266
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Severity Filter
2+
3+
This component implements a [filter component](https://github.com/smithy-security/smithy/blob/main/sdk/component/component.go)
4+
This filter accepts a minimum Severity and adds a "Filtered" enrichment to all findings that don't have at least the min severity.
5+
This component does not care about the severity of each individual vulnerability, it only matches on the Finding Severity.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"log/slog"
8+
"os"
9+
"strings"
10+
"time"
11+
12+
"github.com/smithy-security/smithy/sdk/component"
13+
vf "github.com/smithy-security/smithy/sdk/component/vulnerability-finding"
14+
ocsffindinginfo "github.com/smithy-security/smithy/sdk/gen/ocsf_ext/finding_info/v1"
15+
v1 "github.com/smithy-security/smithy/sdk/gen/ocsf_schema/v1"
16+
"google.golang.org/protobuf/encoding/protojson"
17+
)
18+
19+
var providerName = "severity-filter"
20+
21+
type SeverityFilter struct {
22+
minimumSeverity v1.VulnerabilityFinding_SeverityId
23+
}
24+
25+
func NewSeverityFilter() (*SeverityFilter, error) {
26+
severityStr := os.Getenv("MINIMUM_SEVERITY")
27+
if severityStr == "" {
28+
severityStr = "MEDIUM" // Default to MEDIUM if not specified
29+
}
30+
severityStr = strings.ToUpper(severityStr)
31+
severityId, ok := v1.VulnerabilityFinding_SeverityId_value["SEVERITY_ID_"+severityStr]
32+
if !ok {
33+
return nil, fmt.Errorf("invalid MINIMUM_SEVERITY value: %s. Must be one of: UNKNOWN, INFORMATIONAL, LOW, MEDIUM, HIGH, CRITICAL", severityStr)
34+
}
35+
36+
return &SeverityFilter{
37+
minimumSeverity: v1.VulnerabilityFinding_SeverityId(severityId),
38+
}, nil
39+
}
40+
41+
func (s SeverityFilter) Filter(ctx context.Context, findings []*vf.VulnerabilityFinding) ([]*vf.VulnerabilityFinding, bool, error) {
42+
component.LoggerFromContext(ctx).Info("Running Severity Filter")
43+
findings_filtered := 0
44+
for _, f := range findings {
45+
if f.Finding.SeverityId >= s.minimumSeverity {
46+
enrichment := ocsffindinginfo.Enrichment{
47+
EnrichmentType: ocsffindinginfo.Enrichment_ENRICHMENT_FILTER,
48+
Enrichment: &ocsffindinginfo.Enrichment_Filter{},
49+
}
50+
toBytes, err := protojson.Marshal(&enrichment)
51+
if err != nil {
52+
return nil, false, fmt.Errorf("failed to marshal enrichment %v err: %w", enrichment, err)
53+
}
54+
f.Finding.Enrichments = append(f.Finding.Enrichments, &v1.Enrichment{
55+
Name: providerName,
56+
Provider: &providerName,
57+
Value: string(toBytes),
58+
})
59+
findings_filtered++
60+
}
61+
}
62+
component.LoggerFromContext(ctx).Info("filtered", slog.Int("findings_filtered", findings_filtered))
63+
return findings, findings_filtered > 0, nil
64+
}
65+
66+
func main() {
67+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
68+
defer cancel()
69+
70+
filter, err := NewSeverityFilter()
71+
if err != nil {
72+
log.Fatalf("failed to create severity filter: %v", err)
73+
}
74+
75+
if err := component.RunFilter(ctx, *filter, component.RunnerWithComponentName("Severity Filter")); err != nil {
76+
log.Fatalf("unexpected run error: %v", err)
77+
}
78+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
vf "github.com/smithy-security/smithy/sdk/component/vulnerability-finding"
12+
ocsf "github.com/smithy-security/smithy/sdk/gen/ocsf_schema/v1"
13+
)
14+
15+
func TestSeverityFilter_Filter(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
envSeverity string
19+
findings []*vf.VulnerabilityFinding
20+
expectedFiltered int
21+
expectError bool
22+
}{
23+
{
24+
name: "Valid configuration with HIGH severity",
25+
envSeverity: "HIGH",
26+
findings: []*vf.VulnerabilityFinding{
27+
{
28+
Finding: &ocsf.VulnerabilityFinding{
29+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_CRITICAL,
30+
},
31+
},
32+
{
33+
Finding: &ocsf.VulnerabilityFinding{
34+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_HIGH,
35+
},
36+
},
37+
{
38+
Finding: &ocsf.VulnerabilityFinding{
39+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_MEDIUM,
40+
},
41+
},
42+
{
43+
Finding: &ocsf.VulnerabilityFinding{
44+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_LOW,
45+
},
46+
},
47+
{
48+
Finding: &ocsf.VulnerabilityFinding{
49+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_INFORMATIONAL,
50+
},
51+
},
52+
{
53+
Finding: &ocsf.VulnerabilityFinding{
54+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_UNKNOWN,
55+
},
56+
},
57+
},
58+
expectedFiltered: 2,
59+
expectError: false,
60+
},
61+
{
62+
name: "Valid configuration with MEDIUM severity",
63+
envSeverity: "MEDIUM",
64+
findings: []*vf.VulnerabilityFinding{
65+
{
66+
Finding: &ocsf.VulnerabilityFinding{
67+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_LOW,
68+
},
69+
},
70+
{
71+
Finding: &ocsf.VulnerabilityFinding{
72+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_MEDIUM,
73+
},
74+
},
75+
{
76+
Finding: &ocsf.VulnerabilityFinding{
77+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_HIGH,
78+
},
79+
},
80+
},
81+
expectedFiltered: 2,
82+
expectError: false,
83+
},
84+
{
85+
name: "Invalid configuration with unknown severity",
86+
envSeverity: "INVALID",
87+
findings: []*vf.VulnerabilityFinding{
88+
{
89+
Finding: &ocsf.VulnerabilityFinding{
90+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_CRITICAL,
91+
},
92+
},
93+
},
94+
expectedFiltered: 0,
95+
expectError: true,
96+
},
97+
{
98+
name: "Empty findings list",
99+
envSeverity: "HIGH",
100+
findings: []*vf.VulnerabilityFinding{},
101+
expectedFiltered: 0,
102+
expectError: false,
103+
},
104+
{
105+
name: "Valid configuration with CRITICAL severity",
106+
envSeverity: "CRITICAL",
107+
findings: []*vf.VulnerabilityFinding{
108+
{
109+
Finding: &ocsf.VulnerabilityFinding{
110+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_CRITICAL,
111+
},
112+
},
113+
{
114+
Finding: &ocsf.VulnerabilityFinding{
115+
SeverityId: ocsf.VulnerabilityFinding_SEVERITY_ID_HIGH,
116+
},
117+
},
118+
},
119+
expectedFiltered: 1,
120+
expectError: false,
121+
},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.name, func(t *testing.T) {
126+
os.Setenv("MINIMUM_SEVERITY", tt.envSeverity)
127+
defer os.Unsetenv("MINIMUM_SEVERITY")
128+
129+
filter, err := NewSeverityFilter()
130+
if tt.expectError {
131+
require.Error(t, err)
132+
return
133+
}
134+
require.NoError(t, err)
135+
136+
ctx := context.Background()
137+
filteredFindings, filtered, err := filter.Filter(ctx, tt.findings)
138+
require.NoError(t, err)
139+
filteredFindingsCount := 0
140+
for _, finding := range filteredFindings {
141+
for _, enrichment := range finding.Finding.Enrichments {
142+
if enrichment.Name == providerName {
143+
assert.Equal(t, providerName, *enrichment.Provider)
144+
assert.Equal(t, providerName, enrichment.Name)
145+
assert.NotEmpty(t, enrichment.Value)
146+
filteredFindingsCount++
147+
}
148+
}
149+
}
150+
assert.Equal(t, tt.expectedFiltered, filteredFindingsCount)
151+
assert.Equal(t, tt.expectedFiltered > 0, filtered)
152+
})
153+
}
154+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: severity
2+
description: "Tags all findings below a certain severity as 'Filtered Out'"
3+
type: filter
4+
parameters:
5+
- name: minimum_severity
6+
type: string
7+
value: "High"
8+
steps:
9+
- name: filters
10+
image: components/filters/severity
11+
env_vars:
12+
MINIMUM_SEVERITY: "{{.parameters.minimum_severity}}"
13+
executable: /bin/app
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
git-clone:
2+
- name: "repo_url"
3+
type: "string"
4+
value: "https://github.com/0c34/govwa.git"
5+
- name: "reference"
6+
type: "string"
7+
value: "master"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
description: GoSec based workflow
2+
name: gosec
3+
components:
4+
- component: file://components/targets/git-clone/component.yaml
5+
- component: file://components/scanners/gosec/component.yaml
6+
- component: file://components/scanners/nancy/component.yaml
7+
- component: file://components/enrichers/custom-annotation/component.yaml
8+
- component: file://components/filters/severity/component.yaml
9+
- component: file://components/reporters/json-logger/component.yaml

0 commit comments

Comments
 (0)