Skip to content

Commit 8df660a

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 8df660a

4 files changed

Lines changed: 136 additions & 143 deletions

File tree

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

Lines changed: 21 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,6 @@
77
import sys
88
import tempfile
99

10-
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-
3010
class Converter:
3111
def __init__(self, language, dbDir):
3212
self.language = language
@@ -63,48 +43,30 @@ def runQuery(self, query):
6343
)
6444
return helpers.readData(self.workDir, resultBqrs)
6545

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):
46+
def merge_query_results(self, query, predicate, mergers):
7747
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]),
48+
for row in data:
49+
provenance = row[-1]
50+
namespace = row[0]
51+
target_merger = mergers[1] if provenance.endswith("generated") else mergers[0]
52+
target_merger.add_row(namespace, predicate, row)
53+
54+
def make_extensions(self):
55+
mergers = [
56+
helpers.ExtensionMerger(f"codeql/{self.language}-all"),
57+
helpers.ExtensionMerger(f"codeql/{self.language}-all"),
8958
]
59+
self.merge_query_results("ExtractSummaries.ql", helpers.summaryModelPredicate, mergers)
60+
self.merge_query_results("ExtractSources.ql", helpers.sourceModelPredicate, mergers)
61+
self.merge_query_results("ExtractSinks.ql", helpers.sinkModelPredicate, mergers)
62+
self.merge_query_results("ExtractNeutrals.ql", helpers.neutralModelPredicate, mergers)
63+
return mergers
64+
65+
def run(self):
66+
mergers = self.make_extensions()
9067

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

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

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

Lines changed: 32 additions & 79 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,50 @@ 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
205-
)
186+
self.merge_query_results("CaptureSummaryModels.ql", helpers.summaryModelPredicate, merger)
206187

207-
sinkAddsTo = {}
208188
if self.with_sinks:
209-
sinkAddsTo = self.getAddsTo(
210-
"CaptureSinkModels.ql", helpers.sinkModelPredicate
211-
)
189+
self.merge_query_results("CaptureSinkModels.ql", helpers.sinkModelPredicate, merger)
212190

213-
sourceAddsTo = {}
214191
if self.with_sources:
215-
sourceAddsTo = self.getAddsTo(
216-
"CaptureSourceModels.ql", helpers.sourceModelPredicate
217-
)
192+
self.merge_query_results("CaptureSourceModels.ql", helpers.sourceModelPredicate, merger)
218193

219-
neutralAddsTo = {}
220194
if self.with_neutrals:
221-
neutralAddsTo = self.getAddsTo(
222-
"CaptureNeutralModels.ql", helpers.neutralModelPredicate
223-
)
195+
self.merge_query_results("CaptureNeutralModels.ql", helpers.neutralModelPredicate, merger)
224196

225-
return helpers.merge(summaryAddsTo, sinkAddsTo, sourceAddsTo, neutralAddsTo)
197+
return merger
226198

227-
def makeTypeBasedContent(self):
199+
def make_type_based_extensions(self):
200+
merger = helpers.ExtensionMerger(f"codeql/{self.language}-all")
228201
if self.with_typebased_summaries:
229-
typeBasedSummaryAddsTo = self.getAddsTo(
230-
"CaptureTypeBasedSummaryModels.ql", helpers.summaryModelPredicate
231-
)
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)
202+
self.merge_query_results("CaptureTypeBasedSummaryModels.ql", helpers.summaryModelPredicate, merger)
203+
return merger
251204

252205
def run(self):
253-
content = self.makeContent()
254-
typeBasedContent = self.makeTypeBasedContent()
206+
merger = self.make_extensions()
207+
type_based_extensions = self.make_type_based_extensions()
255208

256209
if self.dry_run:
257210
print("Models as data extensions generated, but not written to file.")
@@ -263,10 +216,10 @@ def run(self):
263216
or self.with_summaries
264217
or self.with_neutrals
265218
):
266-
self.save(content, ".model.yml")
219+
merger.save(self.generated_frameworks, ".model.yml")
267220

268221
if self.with_typebased_summaries:
269-
self.save(typeBasedContent, ".typebased.model.yml")
222+
type_based_extensions.save(self.generated_frameworks, ".typebased.model.yml")
270223

271224

272225
if __name__ == "__main__":

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

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,56 @@
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+
# Helper class for accumulating tuples grouped by namespace and predicate, and generating data
15+
# extensions for them.
16+
class ExtensionMerger:
17+
def __init__(self, pack):
18+
self.pack = pack
19+
self.namespaces = {}
20+
21+
def add_row(self, namespace, predicate, row):
22+
if namespace not in self.namespaces:
23+
self.namespaces[namespace] = {}
24+
if predicate not in self.namespaces[namespace]:
25+
self.namespaces[namespace][predicate] = []
26+
self.namespaces[namespace][predicate].append(row)
27+
28+
# Helper function to yield unique elements from a sorted list.
29+
def uniq(self, sorted_list):
30+
last = None
31+
for element in sorted_list:
32+
if element == last:
33+
continue
34+
yield element
35+
last = element
36+
37+
def save(self, dir, file_extension):
38+
# Create a file for each namespace and save models.
39+
for namespace in self.namespaces:
40+
# Sort and deduplicate rows for each predicate within this namespace.
41+
for predicate in self.namespaces[namespace]:
42+
l = self.namespaces[namespace][predicate]
43+
self.namespaces[namespace][predicate] = list(self.uniq(sorted(l)))
44+
extension = data_extension_file.DataExtensionFile(self.pack)
45+
for predicate in self.namespaces[namespace]:
46+
extension.add_rows(predicate, self.namespaces[namespace][predicate])
47+
# Replace problematic characters with dashes, and collapse multiple dashes.
48+
sanitized_namespace = re.sub(
49+
r"-+", "-", namespace.replace("/", "-").replace(":", "-")
50+
)
51+
target = os.path.join(dir, f"{sanitized_namespace}{file_extension}")
52+
with open(target, "w") as f:
53+
extension.write_yaml(f)
54+
print("Models as data extensions written to " + target)
1655

1756

1857
def remove_dir(dirName):

0 commit comments

Comments
 (0)