Skip to content

Commit 96ec1ed

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 rows are represented as Python tuples, 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 96ec1ed

4 files changed

Lines changed: 162 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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import json
2+
3+
4+
class DataExtensionFile:
5+
"""Simple class for representing a data extension file and writing it to disk in a
6+
pretty-printed YAML format."""
7+
8+
def __init__(self, pack):
9+
self.pack = pack
10+
# predicate -> dict of tuples (used as an ordered set)
11+
self.predicates = {}
12+
13+
def add_rows(self, predicate, rows):
14+
"""Add the rows, which can be any collection of any collection of string/boolean values."""
15+
if predicate not in self.predicates:
16+
self.predicates[predicate] = dict()
17+
d = self.predicates[predicate]
18+
for row in rows:
19+
# To be agnostic about the type of collection used for rows, convert each one to a
20+
# tuple. Insert the tuple as a key in the dict, associating it with a dummy value.
21+
d[tuple(row)] = None
22+
23+
def yaml_for_value(self, value):
24+
# if is boolean:
25+
if isinstance(value, bool):
26+
# json.dumps would produce lower-case "true" or "false". Even though that's valid YAML,
27+
# we have historically used the upper-case variants.
28+
return "True" if value else "False"
29+
return json.dumps(value)
30+
31+
def yaml_for_row(self, row):
32+
return f"[{', '.join(self.yaml_for_value(value) for value in row)}]"
33+
34+
def yaml_for_predicate(self, predicate):
35+
return f""" - addsTo:
36+
pack: {self.pack}
37+
extensible: {predicate}
38+
data:
39+
- {'\n - '.join(self.yaml_for_row(row) for row in self.predicates[predicate].keys())}
40+
"""
41+
42+
def write_yaml(self, f):
43+
# It would be preferable to use a YAML serialization library, but this lets us control the
44+
# formatting e.g. to produce one line per tuple.
45+
f.write("# THIS FILE IS AN AUTO-GENERATED MODELS AS DATA FILE. DO NOT EDIT.\n")
46+
f.write("extensions:\n")
47+
for predicate in sorted(self.predicates.keys()):
48+
f.write(self.yaml_for_predicate(predicate))

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

Lines changed: 44 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,65 @@ 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+
if self.single_file:
181+
merger.add_row(self.single_file, predicate, d)
182+
else:
183+
merger.add_row(namespace, predicate, d)
184+
185+
def make_extensions(self):
186+
merger = helpers.ExtensionMerger(f"codeql/{self.language}-all")
199187

200-
def makeContent(self):
201-
summaryAddsTo = {}
202188
if self.with_summaries:
203-
summaryAddsTo = self.getAddsTo(
204-
"CaptureSummaryModels.ql", helpers.summaryModelPredicate
189+
self.merge_query_results(
190+
"CaptureSummaryModels.ql", helpers.summaryModelPredicate, merger
205191
)
206192

207-
sinkAddsTo = {}
208193
if self.with_sinks:
209-
sinkAddsTo = self.getAddsTo(
210-
"CaptureSinkModels.ql", helpers.sinkModelPredicate
194+
self.merge_query_results(
195+
"CaptureSinkModels.ql", helpers.sinkModelPredicate, merger
211196
)
212197

213-
sourceAddsTo = {}
214198
if self.with_sources:
215-
sourceAddsTo = self.getAddsTo(
216-
"CaptureSourceModels.ql", helpers.sourceModelPredicate
199+
self.merge_query_results(
200+
"CaptureSourceModels.ql", helpers.sourceModelPredicate, merger
217201
)
218202

219-
neutralAddsTo = {}
220203
if self.with_neutrals:
221-
neutralAddsTo = self.getAddsTo(
222-
"CaptureNeutralModels.ql", helpers.neutralModelPredicate
204+
self.merge_query_results(
205+
"CaptureNeutralModels.ql", helpers.neutralModelPredicate, merger
223206
)
224207

225-
return helpers.merge(summaryAddsTo, sinkAddsTo, sourceAddsTo, neutralAddsTo)
208+
return merger
226209

227-
def makeTypeBasedContent(self):
210+
def make_type_based_extensions(self):
211+
merger = helpers.ExtensionMerger(f"codeql/{self.language}-all")
228212
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(":", "-")
213+
self.merge_query_results(
214+
"CaptureTypeBasedSummaryModels.ql",
215+
helpers.summaryModelPredicate,
216+
merger,
246217
)
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)
218+
return merger
251219

252220
def run(self):
253-
content = self.makeContent()
254-
typeBasedContent = self.makeTypeBasedContent()
221+
merger = self.make_extensions()
222+
type_based_extensions = self.make_type_based_extensions()
255223

256224
if self.dry_run:
257225
print("Models as data extensions generated, but not written to file.")
@@ -263,10 +231,12 @@ def run(self):
263231
or self.with_summaries
264232
or self.with_neutrals
265233
):
266-
self.save(content, ".model.yml")
234+
merger.save(self.generated_frameworks, ".model.yml")
267235

268236
if self.with_typebased_summaries:
269-
self.save(typeBasedContent, ".typebased.model.yml")
237+
type_based_extensions.save(
238+
self.generated_frameworks, ".typebased.model.yml"
239+
)
270240

271241

272242
if __name__ == "__main__":

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

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,53 @@
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+
class ExtensionMerger:
16+
"""Helper class for accumulating tuples grouped by namespace and predicate, and generating data
17+
extensions for them."""
18+
19+
def __init__(self, pack):
20+
self.pack = pack
21+
# namespace -> predicate -> dict of tuples (used as an ordered set)
22+
self.namespaces = {}
23+
24+
def add_row(self, namespace, predicate, row):
25+
"""Add the row, which can be any type of collection of string/boolean values, to the set of
26+
tuples for the given namespace and predicate."""
27+
if namespace not in self.namespaces:
28+
self.namespaces[namespace] = {}
29+
if predicate not in self.namespaces[namespace]:
30+
self.namespaces[namespace][predicate] = dict()
31+
self.namespaces[namespace][predicate][tuple(row)] = None
32+
33+
def save(self, dir, file_extension):
34+
"""Save the accumulated rows as data extension files in the specified directory. Each
35+
namespace will result in a separate file, with the file name derived from the namespace,
36+
plus the given file extension."""
37+
# Create a file for each namespace and save models.
38+
for namespace in self.namespaces:
39+
extension = data_extension_file.DataExtensionFile(self.pack)
40+
for predicate in self.namespaces[namespace]:
41+
extension.add_rows(
42+
predicate, self.namespaces[namespace][predicate].keys()
43+
)
44+
# Replace problematic characters with dashes, and collapse multiple dashes.
45+
sanitized_namespace = re.sub(
46+
r"-+", "-", namespace.replace("/", "-").replace(":", "-")
47+
)
48+
target = os.path.join(dir, f"{sanitized_namespace}{file_extension}")
49+
with open(target, "w") as f:
50+
extension.write_yaml(f)
51+
print("Models as data extensions written to " + target)
1652

1753

1854
def remove_dir(dirName):

0 commit comments

Comments
 (0)