-
Notifications
You must be signed in to change notification settings - Fork 153
/
Copy pathscanner.go
154 lines (129 loc) · 4.46 KB
/
scanner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package main
import (
"context"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/mbndr/logo"
)
const tmpPrefix = "clair-scanner-"
type ScannerConfig struct {
ImageName string
Whitelist vulnerabilitiesWhitelist
ClairURL string
ScannerIP string
ReportFile string
WhitelistThreshold string
ReportAll bool
Quiet bool
ExitWhenNoFeatures bool
}
// HTTPClient defines an interface for making HTTP requests
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type Scanner interface {
Scan(config ScannerConfig) []string
}
type DefaultScanner struct {
DockerClient DockerClient
FileSystem FileSystem
HTTPClient HTTPClient
}
func NewDefaultScanner(dockerClient DockerClient, fileSystem FileSystem, httpClient HTTPClient) *DefaultScanner {
return &DefaultScanner{
DockerClient: dockerClient,
FileSystem: fileSystem,
HTTPClient: httpClient,
}
}
func (ds *DefaultScanner) Scan(logger *logo.Logger, config ScannerConfig) []string {
tmpPath := createTmpPath(logger, tmpPrefix)
defer os.RemoveAll(tmpPath)
err := saveDockerImage(ds.DockerClient, config.ImageName, tmpPath)
if err != nil {
log.Fatalf("Error saving Docker image: %v", err)
}
payloadJSON, err := LoadDockerManifest(tmpPath, config.ScannerIP, ds.FileSystem)
if err != nil {
log.Fatalf("Failed to load docker manifest: %s", err)
}
// Start the HTTP file server
server, err := httpFileServer(tmpPath, logger, func(s *http.Server) error {
logger.Infof("Starting HTTP server on %s", s.Addr)
return s.ListenAndServe()
}, 15*time.Second) // Add the timeout argument here
if err != nil {
log.Fatalf("Failed to start HTTP file server: %v", err)
}
// Ensure the server is gracefully shut down on exit
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Errorf("Failed to shut down server gracefully: %v", err)
}
}()
headers := map[string]string{
"Content-Type": "application/json",
}
reportID, err := analyzeContainer(ds.HTTPClient, headers, config.ClairURL, *payloadJSON)
if err != nil {
log.Fatalf("Failed to submit container for analysis: %s", err)
}
successfulResponse, err := waitForSuccessfulResponse(ds.HTTPClient, headers, config.ClairURL, reportID)
if err != nil {
log.Printf("Error waiting for successful response: %v", err)
return nil
}
if successfulResponse.StatusCode != 200 {
log.Printf("Unexpected status code: %d", successfulResponse.StatusCode)
return nil
}
vulnerabilities, err := fetchVulnerabilities(ds.HTTPClient, headers, config.ClairURL, reportID)
if err != nil {
log.Printf("Error fetching vulnerabilities: %v", err)
return nil
}
unapproved := checkForUnapprovedVulnerabilities(config.ImageName, vulnerabilities, config.Whitelist, config.WhitelistThreshold)
// Pass os.Stdout as the writer for console output
reportToConsole(logger, os.Stdout, config.ImageName, vulnerabilities, unapproved, config.ReportAll, config.Quiet)
// Generate JSON report and write to file if necessary
if jsonData, err := reportToFile(config.ImageName, vulnerabilities, unapproved, config.ReportFile); err == nil && jsonData != nil {
if err := os.WriteFile(config.ReportFile, jsonData, 0644); err != nil {
logger.Errorf("Failed to write report to file: %v", err)
}
} else if err != nil {
logger.Errorf("Failed to generate JSON report: %v", err)
}
return unapproved
}
func checkForUnapprovedVulnerabilities(imageName string, vulnerabilities []vulnerabilityInfo, whitelist vulnerabilitiesWhitelist, whitelistThreshold string) []string {
unapproved := []string{}
imageVulnerabilities := getImageVulnerabilities(imageName, whitelist.Images)
for _, vuln := range vulnerabilities {
vulnerable := true
if SeverityMap[vuln.Severity] > SeverityMap[whitelistThreshold] {
vulnerable = false
}
if _, exists := whitelist.GeneralWhitelist[vuln.Vulnerability]; exists {
vulnerable = false
}
if _, exists := imageVulnerabilities[vuln.Vulnerability]; exists {
vulnerable = false
}
if vulnerable {
unapproved = append(unapproved, vuln.Vulnerability)
}
}
return unapproved
}
func getImageVulnerabilities(imageName string, whitelistImageVulnerabilities map[string]map[string]string) map[string]string {
imageWithoutVersion := strings.Split(imageName, ":")[0]
if val, exists := whitelistImageVulnerabilities[imageWithoutVersion]; exists {
return val
}
return nil
}