Skip to content

Commit e8e8657

Browse files
committed
MaD: update generator scripts to be format-agnostic
In preparation for adding support for JSON data extensions, this commit updates the scripts so that they store and build models using an in-memory representation that is independent of the YAML format currently used. This means tuples are represented as Python lists, where previously they were created as YAML-formatted strings in the merging process. Scalar values inside those tuples are represented as Python strings and booleans. Only the DataExtensionFile.write_yaml function now has to deal with YAML formatting concerns.
1 parent ed5f381 commit e8e8657

4 files changed

Lines changed: 155 additions & 137 deletions

File tree

‎misc/scripts/models-as-data/convert_extensions.py‎

Lines changed: 29 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,6 @@
88
import tempfile
99

1010

11-
def quote_if_needed(v):
12-
# string columns
13-
if type(v) is str:
14-
return '"' + v + '"'
15-
# bool column
16-
return str(v)
17-
18-
19-
def parseData(data):
20-
rows = [{}, {}]
21-
for row in data:
22-
d = map(quote_if_needed, row)
23-
provenance = row[-1]
24-
targetRows = rows[1] if provenance.endswith("generated") else rows[0]
25-
helpers.insert_update(targetRows, row[0], " - [" + ", ".join(d) + "]\n")
26-
27-
return rows
28-
29-
3011
class Converter:
3112
def __init__(self, language, dbDir):
3213
self.language = language
@@ -63,48 +44,38 @@ def runQuery(self, query):
6344
)
6445
return helpers.readData(self.workDir, resultBqrs)
6546

66-
def asAddsTo(self, rows, predicate):
67-
extensions = [{}, {}]
68-
for i in range(2):
69-
for key in rows[i]:
70-
extensions[i][key] = helpers.addsToTemplate.format(
71-
f"codeql/{self.language}-all", predicate, rows[i][key]
72-
)
73-
74-
return extensions
75-
76-
def getAddsTo(self, query, predicate):
47+
def merge_query_results(self, query, predicate, mergers):
7748
data = self.runQuery(query)
78-
rows = parseData(data)
79-
return self.asAddsTo(rows, predicate)
80-
81-
def makeContent(self):
82-
summaries = self.getAddsTo("ExtractSummaries.ql", helpers.summaryModelPredicate)
83-
sources = self.getAddsTo("ExtractSources.ql", helpers.sourceModelPredicate)
84-
sinks = self.getAddsTo("ExtractSinks.ql", helpers.sinkModelPredicate)
85-
neutrals = self.getAddsTo("ExtractNeutrals.ql", helpers.neutralModelPredicate)
86-
return [
87-
helpers.merge(sources[0], sinks[0], summaries[0], neutrals[0]),
88-
helpers.merge(sources[1], sinks[1], summaries[1], neutrals[1]),
49+
for row in data:
50+
provenance = row[-1]
51+
namespace = row[0]
52+
target_merger = (
53+
mergers[1] if provenance.endswith("generated") else mergers[0]
54+
)
55+
target_merger.add_row(namespace, predicate, row)
56+
57+
def make_extensions(self):
58+
mergers = [
59+
helpers.ExtensionMerger(f"codeql/{self.language}-all"),
60+
helpers.ExtensionMerger(f"codeql/{self.language}-all"),
8961
]
62+
self.merge_query_results(
63+
"ExtractSummaries.ql", helpers.summaryModelPredicate, mergers
64+
)
65+
self.merge_query_results(
66+
"ExtractSources.ql", helpers.sourceModelPredicate, mergers
67+
)
68+
self.merge_query_results("ExtractSinks.ql", helpers.sinkModelPredicate, mergers)
69+
self.merge_query_results(
70+
"ExtractNeutrals.ql", helpers.neutralModelPredicate, mergers
71+
)
72+
return mergers
73+
74+
def run(self):
75+
mergers = self.make_extensions()
9076

91-
def save(self, extensions):
9277
# Create directory if it doesn't exist
9378
os.makedirs(self.extDir, exist_ok=True)
9479

95-
# Create a file for each namespace and save models.
96-
extensionTemplate = """extensions:
97-
{0}"""
98-
for entry in extensions[0]:
99-
with open(self.extDir + "/" + entry + self.modelFileExtension, "w") as f:
100-
f.write(extensionTemplate.format(extensions[0][entry]))
101-
102-
for entry in extensions[1]:
103-
with open(
104-
self.extDir + "/generated/" + entry + self.modelFileExtension, "w"
105-
) as f:
106-
f.write(extensionTemplate.format(extensions[1][entry]))
107-
108-
def run(self):
109-
extensions = self.makeContent()
110-
self.save(extensions)
80+
mergers[0].save(self.extDir, self.modelFileExtension)
81+
mergers[1].save(self.extDir + "/generated", self.modelFileExtension)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
3+
4+
# Simple class for representing a data extension file and writing it to disk in a pretty-printed YAML format.
5+
class DataExtensionFile:
6+
def __init__(self, pack):
7+
self.pack = pack
8+
self.predicates = {}
9+
10+
def add_rows(self, predicate, rows):
11+
if predicate not in self.predicates:
12+
self.predicates[predicate] = []
13+
self.predicates[predicate].extend(rows)
14+
15+
def yaml_for_value(self, value):
16+
# if is boolean:
17+
if isinstance(value, bool):
18+
# json.dumps would produce lower-case "true" or "false". Even though that's valid YAML,
19+
# we have historically used the upper-case variants.
20+
return "True" if value else "False"
21+
return json.dumps(value)
22+
23+
def yaml_for_row(self, row):
24+
return f"[{', '.join(self.yaml_for_value(value) for value in row)}]"
25+
26+
def yaml_for_predicate(self, predicate):
27+
return f""" - addsTo:
28+
pack: {self.pack}
29+
extensible: {predicate}
30+
data:
31+
- {'\n - '.join(self.yaml_for_row(row) for row in self.predicates[predicate])}
32+
"""
33+
34+
def write_yaml(self, f):
35+
# It would be preferable to use a YAML serialization library, but this lets us control the
36+
# formatting e.g. to produce one line per tuple.
37+
f.write("# THIS FILE IS AN AUTO-GENERATED MODELS AS DATA FILE. DO NOT EDIT.\n")
38+
f.write("extensions:\n")
39+
for predicate in sorted(self.predicates.keys()):
40+
f.write(self.yaml_for_predicate(predicate))

‎misc/scripts/models-as-data/generate_mad.py‎

Lines changed: 41 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,8 @@
66
import subprocess
77
import sys
88
import tempfile
9-
import re
109
import argparse
1110

12-
13-
def quote_if_needed(row):
14-
if row != "true" and row != "false":
15-
return '"' + row + '"'
16-
# subtypes column
17-
return row[0].upper() + row[1:]
18-
19-
20-
def parseData(data):
21-
rows = {}
22-
23-
for row in data:
24-
d = row[0].split(";")
25-
namespace = d[0]
26-
d = map(quote_if_needed, d)
27-
helpers.insert_update(rows, namespace, " - [" + ", ".join(d) + "]\n")
28-
29-
return rows
30-
31-
3211
description = """\
3312
This generates summary, source, sink and neutral models for the code in the database.
3413
The files will be placed in `LANGUAGE/ql/lib/ext/generated/modelgenerator/DIR`"""
@@ -182,76 +161,62 @@ def runQuery(self, query):
182161

183162
return helpers.readData(self.workDir, resultBqrs)
184163

185-
def asAddsTo(self, rows, predicate):
186-
extensions = {}
187-
for key in rows:
188-
extensions[key] = helpers.addsToTemplate.format(
189-
f"codeql/{self.language}-all", predicate, rows[key]
190-
)
191-
return extensions
192-
193-
def getAddsTo(self, query, predicate):
164+
def convert_if_needed(self, value):
165+
# Convert a string value to a boolean if it is "true" or "false". Otherwise, return the original
166+
# value as a string. Since no extensible predicate currently uses integers, don't attempt to
167+
# convert numeric strings to integers.
168+
if value == "true":
169+
return True
170+
if value == "false":
171+
return False
172+
return value
173+
174+
def merge_query_results(self, query, predicate, merger):
194175
data = self.runQuery(query)
195-
rows = parseData(data)
196-
if self.single_file and rows:
197-
rows = {self.single_file: "".join(rows.values())}
198-
return self.asAddsTo(rows, predicate)
176+
for row in data:
177+
d = row[0].split(";")
178+
namespace = d[0]
179+
d = list(map(self.convert_if_needed, d))
180+
merger.add_row(namespace, predicate, d)
181+
182+
def make_extensions(self):
183+
merger = helpers.ExtensionMerger(f"codeql/{self.language}-all")
199184

200-
def makeContent(self):
201-
summaryAddsTo = {}
202185
if self.with_summaries:
203-
summaryAddsTo = self.getAddsTo(
204-
"CaptureSummaryModels.ql", helpers.summaryModelPredicate
186+
self.merge_query_results(
187+
"CaptureSummaryModels.ql", helpers.summaryModelPredicate, merger
205188
)
206189

207-
sinkAddsTo = {}
208190
if self.with_sinks:
209-
sinkAddsTo = self.getAddsTo(
210-
"CaptureSinkModels.ql", helpers.sinkModelPredicate
191+
self.merge_query_results(
192+
"CaptureSinkModels.ql", helpers.sinkModelPredicate, merger
211193
)
212194

213-
sourceAddsTo = {}
214195
if self.with_sources:
215-
sourceAddsTo = self.getAddsTo(
216-
"CaptureSourceModels.ql", helpers.sourceModelPredicate
196+
self.merge_query_results(
197+
"CaptureSourceModels.ql", helpers.sourceModelPredicate, merger
217198
)
218199

219-
neutralAddsTo = {}
220200
if self.with_neutrals:
221-
neutralAddsTo = self.getAddsTo(
222-
"CaptureNeutralModels.ql", helpers.neutralModelPredicate
201+
self.merge_query_results(
202+
"CaptureNeutralModels.ql", helpers.neutralModelPredicate, merger
223203
)
224204

225-
return helpers.merge(summaryAddsTo, sinkAddsTo, sourceAddsTo, neutralAddsTo)
205+
return merger
226206

227-
def makeTypeBasedContent(self):
207+
def make_type_based_extensions(self):
208+
merger = helpers.ExtensionMerger(f"codeql/{self.language}-all")
228209
if self.with_typebased_summaries:
229-
typeBasedSummaryAddsTo = self.getAddsTo(
230-
"CaptureTypeBasedSummaryModels.ql", helpers.summaryModelPredicate
210+
self.merge_query_results(
211+
"CaptureTypeBasedSummaryModels.ql",
212+
helpers.summaryModelPredicate,
213+
merger,
231214
)
232-
else:
233-
typeBasedSummaryAddsTo = {}
234-
235-
return typeBasedSummaryAddsTo
236-
237-
def save(self, extensions, extension):
238-
# Create a file for each namespace and save models.
239-
extensionTemplate = """# THIS FILE IS AN AUTO-GENERATED MODELS AS DATA FILE. DO NOT EDIT.
240-
extensions:
241-
{0}"""
242-
for entry in extensions:
243-
# Replace problematic characters with dashes, and collapse multiple dashes.
244-
sanitizedEntry = re.sub(
245-
r"-+", "-", entry.replace("/", "-").replace(":", "-")
246-
)
247-
target = os.path.join(self.generated_frameworks, sanitizedEntry + extension)
248-
with open(target, "w") as f:
249-
f.write(extensionTemplate.format(extensions[entry]))
250-
print("Models as data extensions written to " + target)
215+
return merger
251216

252217
def run(self):
253-
content = self.makeContent()
254-
typeBasedContent = self.makeTypeBasedContent()
218+
merger = self.make_extensions()
219+
type_based_extensions = self.make_type_based_extensions()
255220

256221
if self.dry_run:
257222
print("Models as data extensions generated, but not written to file.")
@@ -263,10 +228,12 @@ def run(self):
263228
or self.with_summaries
264229
or self.with_neutrals
265230
):
266-
self.save(content, ".model.yml")
231+
merger.save(self.generated_frameworks, ".model.yml")
267232

268233
if self.with_typebased_summaries:
269-
self.save(typeBasedContent, ".typebased.model.yml")
234+
type_based_extensions.save(
235+
self.generated_frameworks, ".typebased.model.yml"
236+
)
270237

271238

272239
if __name__ == "__main__":

‎misc/scripts/models-as-data/helpers.py‎

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,57 @@
22
import os
33
import shutil
44
import subprocess
5+
import re
6+
import data_extension_file
57

68
# Shared strings.
79
summaryModelPredicate = "summaryModel"
810
sinkModelPredicate = "sinkModel"
911
sourceModelPredicate = "sourceModel"
1012
neutralModelPredicate = "neutralModel"
11-
addsToTemplate = """ - addsTo:
12-
pack: {0}
13-
extensible: {1}
14-
data:
15-
{2}"""
13+
14+
15+
# Helper class for accumulating tuples grouped by namespace and predicate, and generating data
16+
# extensions for them.
17+
class ExtensionMerger:
18+
def __init__(self, pack):
19+
self.pack = pack
20+
self.namespaces = {}
21+
22+
def add_row(self, namespace, predicate, row):
23+
if namespace not in self.namespaces:
24+
self.namespaces[namespace] = {}
25+
if predicate not in self.namespaces[namespace]:
26+
self.namespaces[namespace][predicate] = []
27+
self.namespaces[namespace][predicate].append(row)
28+
29+
# Helper function to yield unique elements from a sorted list.
30+
def uniq(self, sorted_list):
31+
last = None
32+
for element in sorted_list:
33+
if element == last:
34+
continue
35+
yield element
36+
last = element
37+
38+
def save(self, dir, file_extension):
39+
# Create a file for each namespace and save models.
40+
for namespace in self.namespaces:
41+
# Sort and deduplicate rows for each predicate within this namespace.
42+
for predicate in self.namespaces[namespace]:
43+
l = self.namespaces[namespace][predicate]
44+
self.namespaces[namespace][predicate] = list(self.uniq(sorted(l)))
45+
extension = data_extension_file.DataExtensionFile(self.pack)
46+
for predicate in self.namespaces[namespace]:
47+
extension.add_rows(predicate, self.namespaces[namespace][predicate])
48+
# Replace problematic characters with dashes, and collapse multiple dashes.
49+
sanitized_namespace = re.sub(
50+
r"-+", "-", namespace.replace("/", "-").replace(":", "-")
51+
)
52+
target = os.path.join(dir, f"{sanitized_namespace}{file_extension}")
53+
with open(target, "w") as f:
54+
extension.write_yaml(f)
55+
print("Models as data extensions written to " + target)
1656

1757

1858
def remove_dir(dirName):

0 commit comments

Comments
 (0)