Skip to content

Commit 2e89b2d

Browse files
committed
battlecard-printer reporter
1 parent 5329691 commit 2e89b2d

File tree

5 files changed

+441
-0
lines changed

5 files changed

+441
-0
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Battlecard Printer Reporter
2+
3+
## Overview
4+
5+
The **battlecard-printer** is a Smithy reporter component that logs a summary of vulnerability findings in a concise "battlecard" format. It is designed to help teams quickly understand the results of security scans, highlighting key metrics such as total findings, enrichments, and findings by tool.
6+
7+
## Features
8+
9+
* Summarizes vulnerability findings from Smithy workflows
10+
* Aggregates enrichments and findings by tool
11+
* Outputs a human-readable battlecard report
12+
* Integrates easily into Smithy workflows
13+
14+
## Usage
15+
16+
### Workflow Integration
17+
18+
To use the battlecard-printer in a Smithy workflow, add it as a reporter component in your workflow YAML:
19+
20+
```yaml
21+
components:
22+
- component: file://components/targets/git-clone/component.yaml
23+
- component: file://components/scanners/mobsfscan/component.yaml
24+
- component: file://components/enrichers/custom-annotation/component.yaml
25+
- component: file://components/reporters/battlecard-printer/component.yaml
26+
```
27+
28+
### Component Configuration
29+
30+
The component is defined as follows:
31+
32+
```yaml
33+
name: battlecard-printer
34+
description: "Logs a summary of vulnerability findings in a battlecard format."
35+
type: reporter
36+
steps:
37+
- name: battlecard-printer
38+
image: components/reporters/battlecard-printer
39+
executable: /bin/app
40+
```
41+
42+
## Output Format
43+
44+
The battlecard-printer generates output similar to:
45+
46+
```
47+
Battlecard Report
48+
=================
49+
Total Findings: 3
50+
Enrichments:
51+
- bar: 1
52+
- foo: 1
53+
Findings By Tool:
54+
- gosec: 2
55+
- trufflehog: 1
56+
```
57+
58+
## How It Works
59+
60+
The reporter:
61+
62+
* Collects all findings from the workflow
63+
* Aggregates enrichments (e.g., custom annotations)
64+
* Counts findings per tool (e.g., gosec, trufflehog)
65+
* Logs the summary using the Smithy logger
66+
67+
## Testing
68+
69+
Unit tests for the battlecard report generation can be found in:
70+
71+
* `internal/reporter/reporter_test.go`
72+
73+
These tests cover:
74+
75+
* Correct aggregation of findings and enrichments
76+
* Output formatting
77+
78+
## Contributing
79+
80+
Contributions and improvements are welcome! Please submit issues or pull requests via GitHub.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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/components/reporters/battlecard-printer/internal/reporter"
13+
)
14+
15+
func main() {
16+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
17+
defer cancel()
18+
19+
if err := Main(ctx); err != nil {
20+
log.Fatalf("unexpected error: %v", err)
21+
}
22+
}
23+
24+
func Main(ctx context.Context, opts ...component.RunnerOption) error {
25+
opts = append(opts, component.RunnerWithComponentName("battlecard-printer"))
26+
27+
if err := component.RunReporter(
28+
ctx,
29+
reporter.NewBattlecardPrinter(),
30+
opts...,
31+
); err != nil {
32+
return errors.Errorf("could not run reporter: %w", err)
33+
}
34+
35+
return nil
36+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name: battlecard-printer
2+
description: "Logs a summary of vulnerability findings in a battlecard format."
3+
type: reporter
4+
steps:
5+
- name: battlecard-printer
6+
image: components/reporters/battlecard-printer
7+
executable: /bin/app
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package reporter
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"maps"
8+
"slices"
9+
10+
vf "github.com/smithy-security/smithy/sdk/component/vulnerability-finding"
11+
componentlogger "github.com/smithy-security/smithy/sdk/logger"
12+
)
13+
14+
// NewBattlecardPrinter returns a new battlecard logger.
15+
func NewBattlecardPrinter() battlecardLogger {
16+
return battlecardLogger{}
17+
}
18+
19+
type battlecardLogger struct{}
20+
21+
// Report logs the findings in json format.
22+
func (j battlecardLogger) Report(
23+
ctx context.Context,
24+
findings []*vf.VulnerabilityFinding,
25+
) error {
26+
logger := componentlogger.
27+
LoggerFromContext(ctx).
28+
With(slog.Int("num_findings", len(findings)))
29+
enrichments := map[string]int{}
30+
for _, finding := range findings {
31+
for _, enrichment := range finding.Finding.Enrichments {
32+
enrichments[enrichment.Name] += 1
33+
}
34+
}
35+
logger.Debug("logging battlecard findings",
36+
slog.Int("num_enrichments", len(enrichments)),
37+
slog.Any("enrichments", enrichments),
38+
)
39+
40+
logger.Info("scan finished with", slog.Int("num_findings", len(findings)))
41+
for enrichmentName, count := range enrichments {
42+
logger.Info("enrichment",
43+
slog.String("name", enrichmentName),
44+
slog.Int("count", count),
45+
)
46+
}
47+
return nil
48+
}
49+
50+
func generateBattlecard(ctx context.Context, findings []*vf.VulnerabilityFinding) string {
51+
logger := componentlogger.
52+
LoggerFromContext(ctx).
53+
With(slog.Int("num_findings", len(findings)))
54+
55+
enrichments := map[string]int{}
56+
tools := map[string]int{}
57+
for _, finding := range findings {
58+
if finding.Finding.FindingInfo.ProductUid == nil {
59+
logger.Warn("finding missing product UID",
60+
slog.String("uid", finding.Finding.FindingInfo.Uid),
61+
)
62+
} else {
63+
tools[*finding.Finding.FindingInfo.ProductUid] += 1
64+
}
65+
for _, enrichment := range finding.Finding.Enrichments {
66+
enrichments[enrichment.Name] += 1
67+
}
68+
}
69+
70+
result := "Battlecard Report\n"
71+
result += "=================\n"
72+
result += "Total Findings: " + fmt.Sprintf("%d\n", len(findings))
73+
result += "Enrichments:\n"
74+
enrichmentKeys := maps.Keys(enrichments)
75+
alphabeticalEnrichments := slices.Sorted(enrichmentKeys)
76+
for _, name := range alphabeticalEnrichments {
77+
result += fmt.Sprintf(" - %s: %d\n", name, enrichments[name])
78+
}
79+
80+
result += "Findings By Tool:\n"
81+
toolKeys := maps.Keys(tools)
82+
alphabeticalTools := slices.Sorted(toolKeys)
83+
for _, toolName := range alphabeticalTools {
84+
result += fmt.Sprintf(" - %s: %d\n", toolName, tools[toolName])
85+
}
86+
87+
return result
88+
}

0 commit comments

Comments
 (0)