-
Notifications
You must be signed in to change notification settings - Fork 80
/
scanner.go
207 lines (190 loc) · 5.65 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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// Package osrelease provides an "os-release" distribution scanner.
package osrelease
import (
"archive/tar"
"bufio"
"bytes"
"context"
"fmt"
"io"
"path/filepath"
"runtime/trace"
"strings"
"github.com/quay/zlog"
"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/label"
"github.com/quay/claircore"
"github.com/quay/claircore/internal/indexer"
"github.com/quay/claircore/pkg/cpe"
)
const (
scannerName = "os-release"
scannerVersion = "v0.0.2"
scannerKind = "distribution"
)
const fpath = `etc/os-release`
var _ indexer.DistributionScanner = (*Scanner)(nil)
var _ indexer.VersionedScanner = (*Scanner)(nil)
// Scanner implements a scanner.DistributionScanner that examines os-release
// files, as documented at
// https://www.freedesktop.org/software/systemd/man/os-release.html
type Scanner struct{}
// Name implements scanner.VersionedScanner.
func (*Scanner) Name() string { return scannerName }
// Version implements scanner.VersionedScanner.
func (*Scanner) Version() string { return scannerVersion }
// Kind implements scanner.VersionedScanner.
func (*Scanner) Kind() string { return scannerKind }
// Scan reports any found os-release Distribution information in the provided
// layer.
//
// It's an expected outcome to return (nil, nil) when the os-release file is not
// present in the layer.
func (s *Scanner) Scan(ctx context.Context, l *claircore.Layer) ([]*claircore.Distribution, error) {
defer trace.StartRegion(ctx, "Scanner.Scan").End()
ctx = baggage.ContextWithValues(ctx,
label.String("component", "osrelease/Scanner.Scan"),
label.String("version", s.Version()),
label.String("layer", l.Hash.String()))
zlog.Debug(ctx).Msg("start")
defer zlog.Debug(ctx).Msg("done")
r, err := l.Reader()
if err != nil {
return nil, fmt.Errorf("osrelease: unable to open layer: %w", err)
}
defer r.Close()
// iterate through the tar and attempt to parse each os-release file encountered.
// on a successful parse return the distribution.
tr := tar.NewReader(r)
hdr, err := tr.Next()
for ; err == nil && ctx.Err() == nil; hdr, err = tr.Next() {
switch hdr.Typeflag {
case tar.TypeReg:
base := filepath.Base(hdr.Name)
if base != "os-release" {
continue
}
d, err := parse(ctx, tr)
if err == nil {
return []*claircore.Distribution{d}, nil
}
}
}
switch err {
case nil:
case io.EOF: // OK
default:
return nil, fmt.Errorf("encountered a tar error: %v", err)
}
if ctx.Err() != nil {
return nil, ctx.Err()
}
zlog.Debug(ctx).Msg("didn't find an os-release file")
return nil, nil
}
// Parse returns the distribution information from the file contents provided on
// r.
func parse(ctx context.Context, r io.Reader) (*claircore.Distribution, error) {
ctx = baggage.ContextWithValues(ctx,
label.String("component", "osrelease/parse"))
defer trace.StartRegion(ctx, "parse").End()
d := claircore.Distribution{
Name: "Linux",
DID: "linux",
}
s := bufio.NewScanner(r)
s.Split(bufio.ScanLines)
for s.Scan() && ctx.Err() == nil {
b := s.Bytes()
switch {
case len(b) == 0:
continue
case b[0] == '#':
continue
}
eq := bytes.IndexRune(b, '=')
if eq == -1 {
return nil, fmt.Errorf("osrelease: malformed line %q", s.Text())
}
key := strings.TrimSpace(string(b[:eq]))
value := strings.TrimSpace(string(b[eq+1:]))
// The value side is defined to follow shell-like quoting rules, which I
// take to mean:
//
// * Within single quotes, no characters are special, and escaping is
// not possible. The only special case that needs to be handled is
// getting a single quote, which is done in shell by ending the
// string, escaping a single quote, then starting a new string.
//
// * Within double quotes, single quotes are not special, but double
// quotes and a handful of other characters are, and almost the entire
// lower-case ASCII alphabet can be escaped to produce various
// codepoints.
//
// With these in mind, the arms of the switch below implement the first
// case and a limited version of the second.
switch value[0] {
case '\'':
value = strings.TrimFunc(value, func(r rune) bool { return r == '\'' })
value = strings.ReplaceAll(value, `'\''`, `'`)
case '"':
// This only implements the metacharacters that are called out in
// the os-release documentation.
value = strings.TrimFunc(value, func(r rune) bool { return r == '"' })
value = strings.NewReplacer(
"\\`", "`",
`\\`, `\`,
`\"`, `"`,
`\$`, `$`,
).Replace(value)
default:
}
switch key {
case "ID":
zlog.Debug(ctx).Msg("found ID")
d.DID = value
case "VERSION_ID":
zlog.Debug(ctx).Msg("found VERSION_ID")
d.VersionID = value
case "BUILD_ID":
case "VARIANT_ID":
case "CPE_NAME":
zlog.Debug(ctx).Msg("found CPE_NAME")
wfn, err := cpe.Unbind(value)
if err != nil {
zlog.Warn(ctx).
Err(err).
Str("value", value).
Msg("failed to unbind the cpe")
break
}
d.CPE = wfn
case "NAME":
zlog.Debug(ctx).Msg("found NAME")
d.Name = value
case "VERSION":
zlog.Debug(ctx).Msg("found VERSION")
d.Version = value
case "ID_LIKE":
case "VERSION_CODENAME":
zlog.Debug(ctx).Msg("found VERISON_CODENAME")
d.VersionCodeName = value
case "PRETTY_NAME":
zlog.Debug(ctx).Msg("found PRETTY_NAME")
d.PrettyName = value
case "REDHAT_BUGZILLA_PRODUCT":
zlog.Debug(ctx).Msg("using RHEL hack")
// This is a dirty hack because the Red Hat OVAL database and the
// CPE contained in the os-release file don't agree.
d.PrettyName = value
}
}
if err := s.Err(); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
zlog.Debug(ctx).Str("name", d.Name).Msg("found dist")
return &d, nil
}