-
Notifications
You must be signed in to change notification settings - Fork 1
/
scan_yocto_packagelist.py
executable file
·196 lines (158 loc) · 5.57 KB
/
scan_yocto_packagelist.py
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
#!/usr/bin/env python
#
# Yocto Security Advisory Tracking Utility
# Used to import the output of "bitbake -s" into cvechecker as a
# watchlist.
def normalize_packagename(name):
"""
Accepts the name of a Yocto package and returns a modified name
known to be used in CVE reports
"""
package_map = {
"cdrtools-native": "cdrtools",
"libpcre" : "pcre",
"libsndfile1" : "libsndfile",
}
if name in package_map:
name = package_map[name]
return name
def useless_packagename(name):
"""
Checks if the package name is too broad to meaningfully search
for, or is known to not correspond to an upstream package (e.g,
core images and tasks). Returns true if those "useless" package
names are found, and False otherwise.
"""
useless_packages = [ "adt-installer", "file", "patch", "time" ]
if name in useless_packages:
#print "Skipping package named ", name
return True
# -native/-nativesdk:
if re.search("-native", name):
#print "Skipping package named ", name
return True
# -cross/-cross-sdk:
if re.search("-cross", name):
#print "Skipping package named ", name
return True
# task-*
if re.search("^task-", name):
#print "Skipping package named ", name
return True
# core-image-*
if re.search("^core-image-", name):
#print "Skipping package named ", name
return True
# poky-image-*
if re.search("^poky-image-", name):
#print "Skipping package named ", name
return True
# -toolchain
if re.search("-toolchain", name):
#print "Skipping package named ", name
return True
# kernels
if re.search("^linux-", name):
#print "Skipping package named ", name
return True
return False
def process_bitbake_s(filename):
"""
Parses the output of bitbake -s, ignoring the "Parsing" header
lines, and returns a dictionary of package names and version
numbers.
"""
f = open(filename)
packages = {}
#print "Processing package list", filename
for line in f:
# Skip the "Parsing" and header lines
if line.startswith("Load") or line.startswith("NOTE: ") or line.startswith("Parsing ") or line.startswith("done.") or line.startswith("Package Name") or line.startswith("==") or len(line) == 1:
continue
# Extract the package name. Example line:
# apmd 0:3.2.2-14-r1
split = line.split(" ", 1)
# first field is the package name
packagename = normalize_packagename(split[0])
if useless_packagename(packagename):
continue
rest = split[1].strip()
# Extract the Poky version number
split = rest.split(" ", 1)
if len(split) == 2:
poky_version = split[1].strip()
else:
poky_version = split[0]
# Strip the package epoch and PR so we're left with the
# upstream version number
version = poky_version.split(':', 1)[1]
version = version.rsplit('-r', 1)[0]
# Strip out cvs/svn/git commit IDs
version = re.split('\+cvs', version)[0]
version = re.split('\+svn', version)[0]
version = re.split('\+git', version)[0]
version = re.split('-git', version)[0]
packages[packagename] = version
return packages
def handle_options(args):
import optparse
parser = optparse.OptionParser(version = "Yocto Security Advisory Scanning Utility",
usage = "%prog [options]")
parser.add_option("-p", help = "Output from bitbake -s",
action = "store", type = "string",
dest = "packages_file")
parser.add_option("-d", help = "Database filename for saving state",
action = "store", type = "string",
dest = "db_filename")
options, args = parser.parse_args(args)
# TODO: Required arguments shouldn't really be processed by optparse
if options.packages_file is None:
print "Error: missing required argument -p"
exit(1)
if options.db_filename is None:
print "Error: missing required argument -d"
exit(1)
return options
# Main
import os, sys, re, datetime, tempfile, subprocess
from pysqlite2 import dbapi2 as sqlite
opts = handle_options(sys.argv)
packages = process_bitbake_s(opts.packages_file)
# Write the package list in CPE format to a temp file, then
# import the temp file into cvechecker as a "watchlist":
t1 = tempfile.NamedTemporaryFile(delete=False)
for pkg, ver in sorted( packages.items() ):
t1.write( "cpe:/a:%s:%s:%s:::\n" % (pkg, pkg, ver) )
t1.close()
os.system( "cvechecker -w %s > /dev/null" % (t1.name) )
os.unlink(t1.name)
newdb = True
if os.path.isfile(opts.db_filename):
print "Note: %s already exists - going to add new entries to this database" % (opts.db_filename)
newdb = False
dbconn = sqlite.connect(opts.db_filename)
db = dbconn.cursor()
if newdb:
print "Creating advisories table"
db.execute('CREATE TABLE advisories (id INTEGER PRIMARY KEY, package TEXT, cve TEXT, cveurl TEXT, status TEXT, last_modified_at DATETIME)')
dbconn.commit()
# Run a cvechecker vulnerability report and extract the
# package name and CVE ID
t2 = tempfile.NamedTemporaryFile(delete=False)
# tail -n +2 strips the first line of output (a csv header)
os.system( "cvechecker -rYC | tail -n +2 | sort -u > %s" % (t2.name) )
for line in t2:
pkg = line.split(':')[3]
cve = line.split(',')[3]
cveurl = "http://web.nvd.nist.gov/view/vuln/detail?vulnId=%s" % (cve)
# Skip if this would be a duplicate entry
db.execute( 'SELECT COUNT(*) FROM advisories WHERE package=? AND cve=?', (pkg, cve) )
res = db.fetchone()[0]
if res is 0:
print "Adding new entry %s for %s" % (cve, pkg)
now = datetime.datetime.now()
db.execute( 'INSERT INTO advisories (id, package, cve, cveurl, status, last_modified_at) VALUES(NULL, ?, ?, ?, "NEW", ?)', (pkg, cve, cveurl, now) )
else:
print "Skipping duplicate %s for %s" % (cve, pkg)
dbconn.commit()
db.close()