Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
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
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
MOCKGEN := go run go.uber.org/mock/mockgen
# go.modのバージョンを使うと、missing go.sum entry for module providing package...エラーが出る
MOCKGEN := go run go.uber.org/mock/mockgen@v0.6.0
.PHONY: test
test:
go test ./... -coverprofile=coverage.txt -covermode=count
test/cli:
go test ./cli/... -coverprofile=coverage.txt -covermode=count
test-container:
docker build -t canarycage/test-container test-container
push-test-container: test-container
Expand All @@ -13,16 +16,20 @@ mocks: go.sum \
mocks/mock_awsiface/iface.go \
mocks/mock_types/iface.go \
mocks/mock_upgrade/upgrade.go \
mocks/mock_audit/scanner.go \
mocks/mock_task/task.go \
mocks/mock_taskset/taskset.go \
mocks/mock_task/factory.go \
mocks/mock_rollout/executor.go
mocks/mock_rollout/executor.go \
mocks/mock_logger/logger.go
mocks/mock_awsiface/iface.go: awsiface/iface.go
$(MOCKGEN) -source=./awsiface/iface.go > mocks/mock_awsiface/iface.go
mocks/mock_types/iface.go: types/iface.go
$(MOCKGEN) -source=./types/iface.go > mocks/mock_types/iface.go
mocks/mock_upgrade/upgrade.go: cli/cage/upgrade/upgrade.go
$(MOCKGEN) -source=./cli/cage/upgrade/upgrade.go > mocks/mock_upgrade/upgrade.go
mocks/mock_audit/scanner.go: cli/cage/audit/scanner.go
$(MOCKGEN) -source=./cli/cage/audit/scanner.go > mocks/mock_audit/scanner.go
mocks/mock_task/task.go: task/task.go
$(MOCKGEN) -source=./task/task.go > mocks/mock_task/task.go
mocks/mock_taskset/taskset.go: taskset/taskset.go
Expand All @@ -31,4 +38,6 @@ mocks/mock_task/factory.go: task/factory.go
$(MOCKGEN) -source=./task/factory.go > mocks/mock_task/factory.go
mocks/mock_rollout/executor.go: rollout/executor.go
$(MOCKGEN) -source=./rollout/executor.go > mocks/mock_rollout/executor.go
mocks/mock_logger/logger.go: logger/logger.go
$(MOCKGEN) -source=./logger/logger.go > mocks/mock_logger/logger.go
.PHONY: mocks
17 changes: 17 additions & 0 deletions awsiface/conf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package awsiface

import (
"context"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
)

// coverage cheat: always use MustLoadConfig to avoid error handling repetition
func MustLoadConfig(ctx context.Context, opts ...func(*config.LoadOptions) error) aws.Config {
cfg, err := config.LoadDefaultConfig(ctx, opts...)
if err != nil {
panic(err)
}
return cfg
}
59 changes: 59 additions & 0 deletions awsiface/conf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package awsiface

import (
"context"
"errors"
"testing"

"github.com/aws/aws-sdk-go-v2/config"
)

func TestMustLoadConfig_Success(t *testing.T) {
ctx := context.Background()

// This should not panic in normal circumstances
defer func() {
if r := recover(); r != nil {
t.Errorf("MustLoadConfig panicked unexpectedly: %v", r)
}
}()

cfg := MustLoadConfig(ctx)

if cfg.Region == "" && cfg.Credentials == nil {
t.Log("Config loaded (region or credentials may be empty in test environment)")
}
}

func TestMustLoadConfig_WithOptions(t *testing.T) {
ctx := context.Background()

defer func() {
if r := recover(); r != nil {
t.Errorf("MustLoadConfig with options panicked unexpectedly: %v", r)
}
}()

cfg := MustLoadConfig(ctx, config.WithRegion("us-west-2"))

if cfg.Region != "us-west-2" {
t.Errorf("Expected region us-west-2, got %s", cfg.Region)
}
}

func TestMustLoadConfig_Panic(t *testing.T) {
ctx := context.Background()

defer func() {
if r := recover(); r == nil {
t.Error("Expected MustLoadConfig to panic with invalid option, but it didn't")
}
}()

// Pass an option that returns an error to trigger panic
invalidOpt := func(*config.LoadOptions) error {
return errors.New("forced error")
}

MustLoadConfig(ctx, invalidOpt)
}
5 changes: 5 additions & 0 deletions awsiface/iface.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ecr"
"github.com/aws/aws-sdk-go-v2/service/ecs"
elbv2 "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
)
Expand All @@ -23,6 +24,10 @@ type (
StopTask(ctx context.Context, params *ecs.StopTaskInput, optFns ...func(*ecs.Options)) (*ecs.StopTaskOutput, error)
DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error)
}
EcrClient interface {
BatchGetImage(ctx context.Context, params *ecr.BatchGetImageInput, optFns ...func(*ecr.Options)) (*ecr.BatchGetImageOutput, error)
DescribeImageScanFindings(ctx context.Context, params *ecr.DescribeImageScanFindingsInput, optFns ...func(*ecr.Options)) (*ecr.DescribeImageScanFindingsOutput, error)
}
AlbClient interface {
DescribeTargetGroups(ctx context.Context, params *elbv2.DescribeTargetGroupsInput, optFns ...func(*elbv2.Options)) (*elbv2.DescribeTargetGroupsOutput, error)
DescribeTargetHealth(ctx context.Context, params *elbv2.DescribeTargetHealthInput, optFns ...func(*elbv2.Options)) (*elbv2.DescribeTargetHealthOutput, error)
Expand Down
165 changes: 165 additions & 0 deletions cli/cage/audit/aggregator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package audit

import (
"fmt"

ecrtypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
"github.com/loilo-inc/canarycage/logger"
)

type aggregater struct {
cves map[string]ecrtypes.ImageScanFinding
cveToSeverity map[string]string
cveToContainers map[string][]string
// container name to summaries
summaries map[string][]*ScanResultSummary
}

func NewAggregater() *aggregater {
return &aggregater{
cves: make(map[string]ecrtypes.ImageScanFinding),
cveToSeverity: make(map[string]string),
cveToContainers: make(map[string][]string),
summaries: make(map[string][]*ScanResultSummary)}
}

func (a *aggregater) Add(r *ScanResult) {
container := r.ContainerName
if r.Err != nil {
a.summaries[container] = append(a.summaries[container], &ScanResultSummary{
ContainerName: container,
Status: "ERROR",
})
return
} else if r.ImageScanFindings == nil {
a.summaries[container] = append(a.summaries[container], &ScanResultSummary{
ContainerName: container,
Status: "N/A",
})
return
}
summary := summaryScanResult(r)
a.summaries[container] = append(a.summaries[container], summary)
for _, f := range r.ImageScanFindings.Findings {
if _, exists := a.cves[*f.Name]; !exists {
a.cves[*f.Name] = f
a.cveToSeverity[*f.Name] = string(f.Severity)
a.cveToContainers[*f.Name] = append(a.cveToContainers[*f.Name], container)
}
}
}

type AggregateResult struct {
CriticalCount int32
HighCount int32
MediumCount int32
LowCount int32
InfoCount int32
TotalCount int32
HighestSeverity ecrtypes.FindingSeverity
}

func (a *aggregater) SummarizeTotal() *AggregateResult {
result := &AggregateResult{}
highest := ecrtypes.FindingSeverityInformational
for cve := range a.cves {
severity := a.cveToSeverity[cve]
switch severity {
case string(ecrtypes.FindingSeverityCritical):
result.CriticalCount++
case string(ecrtypes.FindingSeverityHigh):
result.HighCount++
case string(ecrtypes.FindingSeverityMedium):
result.MediumCount++
case string(ecrtypes.FindingSeverityLow):
result.LowCount++
case string(ecrtypes.FindingSeverityInformational):
result.InfoCount++
}
}
if result.CriticalCount > 0 {
highest = ecrtypes.FindingSeverityCritical
} else if result.HighCount > 0 {
highest = ecrtypes.FindingSeverityHigh
} else if result.MediumCount > 0 {
highest = ecrtypes.FindingSeverityMedium
} else if result.LowCount > 0 {
highest = ecrtypes.FindingSeverityLow
} else {
highest = ecrtypes.FindingSeverityInformational
}
result.HighestSeverity = highest
result.TotalCount = int32(len(a.cves))
return result
}

type SeverityCount struct {
Severity ecrtypes.FindingSeverity
Count int
}

func (a *AggregateResult) SeverityCounts() []SeverityCount {
return []SeverityCount{
{Severity: ecrtypes.FindingSeverityInformational, Count: int(a.InfoCount)},
{Severity: ecrtypes.FindingSeverityLow, Count: int(a.LowCount)},
{Severity: ecrtypes.FindingSeverityMedium, Count: int(a.MediumCount)},
{Severity: ecrtypes.FindingSeverityHigh, Count: int(a.HighCount)},
{Severity: ecrtypes.FindingSeverityCritical, Count: int(a.CriticalCount)},
}
}

func (a *aggregater) TotalCVECount() int {
return len(a.cves)
}

func (a *aggregater) CriticalCves() []ecrtypes.ImageScanFinding {
return a.filterCvesBySeverity(ecrtypes.FindingSeverityCritical)
}

func (a *aggregater) HighCves() []ecrtypes.ImageScanFinding {
return a.filterCvesBySeverity(ecrtypes.FindingSeverityHigh)
}

func (a *aggregater) MediumCves() []ecrtypes.ImageScanFinding {
return a.filterCvesBySeverity(ecrtypes.FindingSeverityMedium)
}

func (a *aggregater) filterCvesBySeverity(severity ecrtypes.FindingSeverity) []ecrtypes.ImageScanFinding {
var cves []ecrtypes.ImageScanFinding
for cve, sev := range a.cveToSeverity {
if sev == string(severity) {
cves = append(cves, a.cves[cve])
}
}
return cves
}

func (a *aggregater) GetVulnContainers(cveName string) []string {
containersSet, exists := a.cveToContainers[cveName]
if !exists {
return []string{}
}
return containersSet
}

type severityPrinter struct {
severity ecrtypes.FindingSeverity
color logger.Color
}

func (s *severityPrinter) Sprintf(format string, a ...any) string {
switch s.severity {
case ecrtypes.FindingSeverityCritical:
return s.color.Magentaf(format, a...)
case ecrtypes.FindingSeverityHigh:
return s.color.Redf(format, a...)
case ecrtypes.FindingSeverityMedium:
return s.color.Yellowf(format, a...)
default:
return fmt.Sprintf(format, a...)
}
}

func (s *severityPrinter) BSprintf(format string, a ...any) string {
return s.color.Bold(s.Sprintf(format, a...))
}
Loading
Loading