Skip to content

Commit cba79aa

Browse files
author
sg
committed
reachability enricher
1 parent 9836120 commit cba79aa

File tree

18 files changed

+112273
-0
lines changed

18 files changed

+112273
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# custom-annotation
2+
3+
This component implements an [enricher](https://github.com/smithy-security/smithy/blob/main/sdk/component/component.go)
4+
This enricher performs reachability analysis using [atom](https://github.com/AppThreat/atom).
5+
6+
## Environment variables
7+
8+
The component uses environment variables for configuration.
9+
10+
It requires the component
11+
environment variables defined [here](https://github.com/smithy-security/smithy/blob/main/sdk/README.md#component) as well as the following:
12+
13+
| Environment Variable | Type | Required | Default | Description |
14+
|----------------------------|--------|----------|---------|-------------------------------------------------------------------------|
15+
| ATOM\_FILE\_PATH | string | yes | - | Path to the file where atom has produced reachable slices. |
16+
17+
## How to run
18+
19+
Execute:
20+
21+
```shell
22+
docker-compose up --build --force-recreate --remove-orphans
23+
```
24+
25+
Then shutdown with:
26+
27+
```shell
28+
docker-compose down --rmi all
29+
```
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
"time"
7+
8+
"github.com/go-errors/errors"
9+
10+
"github.com/smithy-security/smithy/sdk/component"
11+
12+
"github.com/smithy-security/smithy/new-components/enrichers/reachability/internal/annotation"
13+
"github.com/smithy-security/smithy/new-components/enrichers/reachability/internal/conf"
14+
)
15+
16+
func main() {
17+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
18+
defer cancel()
19+
20+
if err := Main(ctx); err != nil {
21+
log.Fatalf("unexpected error: %v", err)
22+
}
23+
}
24+
25+
func Main(ctx context.Context, opts ...component.RunnerOption) error {
26+
opts = append(opts, component.RunnerWithComponentName("reachability"))
27+
28+
config, err := conf.New()
29+
if err != nil {
30+
return errors.Errorf("could not initialiaze config, err: %w", err)
31+
}
32+
annotator := annotation.NewReachabilityAnnotator(config)
33+
34+
if err := component.RunEnricher(ctx, annotator, opts...); err != nil {
35+
return errors.Errorf("error enriching reachability annotation: %w", err)
36+
}
37+
38+
return nil
39+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
services:
2+
enricher:
3+
build:
4+
context: ../..
5+
args:
6+
- COMPONENT_PATH=enrichers/custom-annotation
7+
- COMPONENT_BINARY_SOURCE_PATH=cmd/main.go
8+
platform: linux/amd64
9+
env_file:
10+
- .env
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package annotation
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
8+
"github.com/go-errors/errors"
9+
10+
"github.com/smithy-security/smithy/sdk/component"
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+
"github.com/smithy-security/smithy/new-components/enrichers/reachability/internal/atom"
15+
"github.com/smithy-security/smithy/new-components/enrichers/reachability/internal/atom/purl"
16+
"github.com/smithy-security/smithy/new-components/enrichers/reachability/internal/conf"
17+
"github.com/smithy-security/smithy/new-components/enrichers/reachability/internal/logging"
18+
"github.com/smithy-security/smithy/new-components/enrichers/reachability/internal/search"
19+
)
20+
21+
type (
22+
reachabilityAnnotator struct {
23+
cfg *conf.Conf
24+
atomReader *atom.Reader
25+
annotationName string
26+
providerName string
27+
}
28+
)
29+
30+
// NewReachabilityAnnotator returns a new reachability enricher.
31+
func NewReachabilityAnnotator(cfg *conf.Conf) *reachabilityAnnotator {
32+
return &reachabilityAnnotator{
33+
cfg: cfg,
34+
annotationName: "Reachable-Code",
35+
providerName: "reachability-enricher",
36+
}
37+
}
38+
39+
// Annotate adds annotated values to passed findings.
40+
func (ca *reachabilityAnnotator) Annotate(
41+
ctx context.Context,
42+
findings []*vf.VulnerabilityFinding,
43+
) ([]*vf.VulnerabilityFinding, error) {
44+
var (
45+
logger = component.LoggerFromContext(ctx).
46+
With(slog.Int("num_findings", len(findings))).
47+
With(slog.String("provider_name", ca.providerName))
48+
)
49+
50+
logger.Debug("preparing to annotate findings...")
51+
52+
purlParser, err := purl.NewParser()
53+
if err != nil {
54+
return nil, errors.Errorf("could not initialize purl parser: %w", err)
55+
}
56+
57+
ar, err := atom.NewReader(ca.cfg.ATOMFilePath, purlParser)
58+
if err != nil {
59+
return nil, errors.Errorf("could not initialize atom reader: %w", err)
60+
}
61+
ca.atomReader = ar
62+
findings, err = ca.Enrich(ctx, findings)
63+
if err != nil {
64+
return nil, errors.Errorf("could not enrich findings err: %w", err)
65+
}
66+
logger.Debug("findings annotated successfully!")
67+
return findings, nil
68+
}
69+
70+
// Enrich looks for untagged inputs and processes them outputting if any of them is reachable.
71+
// The reachability checks leverage atom - https://github.com/AppThreat/atom.
72+
func (ca *reachabilityAnnotator) Enrich(ctx context.Context, findings []*vf.VulnerabilityFinding) ([]*vf.VulnerabilityFinding, error) {
73+
var (
74+
logger = logging.FromContext(ctx).With(
75+
slog.String("atom_file_path", ca.cfg.ATOMFilePath),
76+
)
77+
)
78+
79+
logger.Debug("running enrichment step")
80+
logger.Debug("preparing to read response...")
81+
82+
logger = logger.With(slog.Int("num_tagged_resources", len(findings)))
83+
logger.Debug("preparing to read atom file...")
84+
85+
reachablesRes, err := ca.atomReader.Read(ctx)
86+
if err != nil {
87+
return nil, fmt.Errorf("could not read atom reachables from path %s: %w", ca.cfg.ATOMFilePath, err)
88+
}
89+
90+
logger = logger.With(slog.Int("num_atom_reachables", len(reachablesRes.Reachables)))
91+
logger.Debug("successfully read atom file!")
92+
logger.Debug("preparing to check for reachable purls...")
93+
94+
reachablePurls, err := ca.atomReader.ReachablePurls(ctx, reachablesRes)
95+
if err != nil {
96+
return nil, fmt.Errorf("could not get reachable purls: %w", err)
97+
}
98+
99+
logger = logger.With(slog.Int("num_reachable_purls", len(reachablePurls)))
100+
logger.Debug("successfully checked for reachable purls!")
101+
logger.Debug("preparing to create a new searcher...")
102+
103+
searcher, err := search.NewSearcher(reachablesRes.Reachables, reachablePurls)
104+
if err != nil {
105+
return nil, fmt.Errorf("could not create searcher: %w", err)
106+
}
107+
108+
logger.Debug("successfully created a new searcher!")
109+
logger.Debug("preparing to check for reachable targets...")
110+
numEnriched := 0
111+
numReachable := 0
112+
atomPurlParser, err := purl.NewParser()
113+
if err != nil {
114+
return nil, errors.Errorf("could not initialize atom purl parser err: %w", err)
115+
}
116+
for idx, finding := range findings {
117+
logger := logger.With(
118+
slog.String("vendor", *finding.Finding.FindingInfo.ProductUid),
119+
slog.Any("scan_id", finding.ID),
120+
slog.Int("num_vulns", len(finding.Finding.Vulnerabilities)),
121+
)
122+
logger.Debug("preparing to enrich issues in target...")
123+
for _, vuln := range finding.Finding.Vulnerabilities {
124+
for _, pkg := range vuln.AffectedPackages {
125+
parsedPurls, err := atomPurlParser.ParsePurl(*pkg.Purl)
126+
if err != nil {
127+
logger.Error(
128+
"could not search affected package. Continuing...",
129+
slog.String("err", err.Error()))
130+
continue
131+
}
132+
var reachable bool
133+
var reachableEnrichment *ocsf.Enrichment
134+
for _, p := range parsedPurls {
135+
re, reached, err := ca.isReachable(searcher, p)
136+
if err != nil {
137+
logger.Error(
138+
"could not search affected package. Continuing...",
139+
slog.String("err", err.Error()),
140+
)
141+
continue
142+
}
143+
reachable = reached
144+
reachableEnrichment = re
145+
if reached {
146+
break
147+
}
148+
}
149+
if reachable {
150+
numReachable += 1
151+
}
152+
numEnriched += 1
153+
findings[idx].Finding.Enrichments = append(findings[idx].Finding.Enrichments, reachableEnrichment)
154+
}
155+
for _, code := range vuln.AffectedCode {
156+
reachableEnrichment, reachable, err := ca.isReachable(searcher, ca.makeCodeString(code))
157+
if err != nil {
158+
logger.Error(
159+
"could not search affected package. Continuing...",
160+
slog.String("err", err.Error()),
161+
)
162+
}
163+
if reachable {
164+
numReachable += 1
165+
}
166+
numEnriched += 1
167+
findings[idx].Finding.Enrichments = append(findings[idx].Finding.Enrichments, reachableEnrichment)
168+
}
169+
}
170+
171+
logger = logger.With(slog.Int("num_enriched_issues", numEnriched))
172+
logger = logger.With(slog.Int("num_reachable_issues", numReachable))
173+
logger.Debug("successfully enriched issues in target!")
174+
}
175+
logger.Debug("completed enrichment step!")
176+
return findings, nil
177+
}
178+
179+
func (ca *reachabilityAnnotator) makeCodeString(c *ocsf.AffectedCode) string {
180+
return fmt.Sprintf("%s:%d-%d", *c.File.Path, *c.StartLine, *c.EndLine)
181+
}
182+
183+
func (ca *reachabilityAnnotator) isReachable(searcher *search.Searcher, target string) (*ocsf.Enrichment, bool, error) {
184+
// Search.
185+
ok, err := searcher.Search(target)
186+
if err != nil {
187+
return nil, false, err
188+
}
189+
return &ocsf.Enrichment{
190+
Name: ca.annotationName,
191+
Value: fmt.Sprintf("%t", ok),
192+
Provider: &ca.providerName,
193+
}, ok, nil
194+
}

0 commit comments

Comments
 (0)