github.com/quay/claircore@v1.5.28/alpine/distributionscanner.go (about) 1 package alpine 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "io/fs" 9 "regexp" 10 "runtime/trace" 11 "strings" 12 13 "github.com/quay/zlog" 14 15 "github.com/quay/claircore" 16 "github.com/quay/claircore/indexer" 17 "github.com/quay/claircore/osrelease" 18 ) 19 20 // Alpine linux has patch releases but their security database 21 // aggregates security information by major release. We choose 22 // to normalize detected distributions into major.minor releases and 23 // parse vulnerabilities into major.minor releases 24 25 const ( 26 scannerName = "alpine" 27 scannerVersion = "3" 28 scannerKind = "distribution" 29 30 issuePath = `etc/issue` 31 32 edgeVersion = `edge` 33 edgePrettyName = `Alpine Linux edge` 34 ) 35 36 var ( 37 _ indexer.DistributionScanner = (*DistributionScanner)(nil) 38 _ indexer.VersionedScanner = (*DistributionScanner)(nil) 39 40 issueRegexp = regexp.MustCompile(`Alpine Linux ([[:digit:]]+\.[[:digit:]]+)`) 41 edgeIssueRegexp = regexp.MustCompile(`Alpine Linux [[:digit:]]+\.\w+ \(edge\)`) 42 ) 43 44 // DistributionScanner attempts to discover if a layer 45 // displays characteristics of a alpine distribution 46 type DistributionScanner struct{} 47 48 // Name implements scanner.VersionedScanner. 49 func (*DistributionScanner) Name() string { return scannerName } 50 51 // Version implements scanner.VersionedScanner. 52 func (*DistributionScanner) Version() string { return scannerVersion } 53 54 // Kind implements scanner.VersionedScanner. 55 func (*DistributionScanner) Kind() string { return scannerKind } 56 57 // Scan will inspect the layer for an os-release or issue file 58 // and perform a regex match for keywords indicating the associated alpine release 59 // 60 // If neither file is found a (nil, nil) is returned. 61 // If the files are found but all regexp fail to match an empty slice is returned. 62 func (s *DistributionScanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Distribution, error) { 63 defer trace.StartRegion(ctx, "Scanner.Scan").End() 64 ctx = zlog.ContextWithValues(ctx, 65 "component", "alpine/DistributionScanner.Scan", 66 "version", s.Version(), 67 "layer", l.Hash.String()) 68 zlog.Debug(ctx).Msg("start") 69 defer zlog.Debug(ctx).Msg("done") 70 sys, err := l.FS() 71 if err != nil { 72 return nil, fmt.Errorf("alpine: unable to open layer: %w", err) 73 } 74 return s.scanFs(ctx, sys) 75 } 76 77 func (*DistributionScanner) scanFs(ctx context.Context, sys fs.FS) (d []*claircore.Distribution, err error) { 78 for _, f := range []distFunc{readOSRelease, readIssue} { 79 dist, err := f(ctx, sys) 80 if err != nil { 81 return nil, err 82 } 83 if dist != nil { 84 return []*claircore.Distribution{dist}, nil 85 } 86 } 87 88 // Found nothing. 89 return nil, nil 90 } 91 92 type distFunc func(context.Context, fs.FS) (*claircore.Distribution, error) 93 94 // ReadOSRelease looks for the distribution in an os-release file, if it exists. 95 func readOSRelease(ctx context.Context, sys fs.FS) (*claircore.Distribution, error) { 96 b, err := fs.ReadFile(sys, osrelease.Path) 97 switch { 98 case errors.Is(err, nil): 99 // parse here 100 m, err := osrelease.Parse(ctx, bytes.NewReader(b)) 101 if err != nil { 102 return nil, err 103 } 104 if id := m[`ID`]; id != `alpine` { 105 zlog.Debug(ctx).Str("id", id).Msg("seemingly not alpine") 106 break 107 } 108 vid := m[`VERSION_ID`] 109 idx := strings.LastIndexByte(vid, '.') 110 if idx == -1 { 111 zlog.Debug(ctx).Str("val", vid).Msg("martian VERSION_ID") 112 break 113 } 114 v := vid[:idx] 115 if m[`PRETTY_NAME`] == edgePrettyName { 116 v = edgeVersion 117 } 118 return &claircore.Distribution{ 119 Name: m[`NAME`], 120 DID: m[`ID`], 121 Version: v, 122 // BUG(hank) The current version omit the VERSION_ID data. Need to 123 // investigate why. Probably because it's not in the etc/issue 124 // file. 125 // VersionID: vid, 126 PrettyName: m[`PRETTY_NAME`], 127 }, nil 128 case errors.Is(err, fs.ErrNotExist): 129 zlog.Debug(ctx). 130 Str("path", osrelease.Path). 131 Msg("file doesn't exist") 132 default: 133 return nil, err 134 } 135 136 // Found nothing. 137 return nil, nil 138 } 139 140 // ReadIssue looks for the distribution in an issue file, if it exists. 141 func readIssue(ctx context.Context, sys fs.FS) (*claircore.Distribution, error) { 142 b, err := fs.ReadFile(sys, issuePath) 143 switch { 144 case errors.Is(err, nil): 145 if isEdge := edgeIssueRegexp.Match(b); isEdge { 146 return &claircore.Distribution{ 147 Name: `Alpine Linux`, 148 DID: `alpine`, 149 Version: edgeVersion, 150 PrettyName: edgePrettyName, 151 }, nil 152 } 153 154 ms := issueRegexp.FindSubmatch(b) 155 if ms == nil { 156 zlog.Debug(ctx).Msg("seemingly not alpine") 157 break 158 } 159 v := string(ms[1]) 160 return &claircore.Distribution{ 161 Name: `Alpine Linux`, 162 DID: `alpine`, 163 Version: v, 164 PrettyName: fmt.Sprintf(`Alpine Linux v%s`, v), 165 }, nil 166 case errors.Is(err, fs.ErrNotExist): 167 zlog.Debug(ctx). 168 Str("path", issuePath). 169 Msg("file doesn't exist") 170 default: 171 return nil, err 172 } 173 174 // Found nothing. 175 return nil, nil 176 }