Skip to content

Commit 781e068

Browse files
authored
Merge pull request #22798 from github/nickrolfe/mad-generator-json
MaD: update generator scripts to support writing JSON data extensions
2 parents bc53c78 + 3872102 commit 781e068

5 files changed

Lines changed: 215 additions & 141 deletions

File tree

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ def generate_models(config, args, project: Project, database_dir: str) -> None:
241241
generator.with_summaries = should_generate_summaries(project)
242242
generator.threads = args.codeql_threads
243243
generator.ram = args.codeql_ram
244+
generator.extension_format = args.extension_format
244245
if config.get("single-file", False):
245246
generator.single_file = name
246247
else:
@@ -430,10 +431,11 @@ def download_and_decompress(analyzed_database: dict) -> str:
430431
def clean_up_mad_destination_for_project(config, name: str):
431432
target = pathlib.Path(config["destination"], name)
432433
if config.get("single-file", False):
433-
target = target.with_suffix(".model.yml")
434-
if target.exists():
435-
print(f"Deleting existing MaD file at {target}")
436-
target.unlink()
434+
for extension in (".model.json", ".model.yml"):
435+
model_target = target.with_suffix(extension)
436+
if model_target.exists():
437+
print(f"Deleting existing MaD file at {model_target}")
438+
model_target.unlink()
437439
elif target.exists():
438440
print(f"Deleting existing MaD directory at {target}")
439441
shutil.rmtree(target, ignore_errors=True)
@@ -543,6 +545,12 @@ def main(config, args) -> None:
543545
help="What `--threads` value to pass to `codeql` (default %(default)s)",
544546
default=0,
545547
)
548+
parser.add_argument(
549+
"--extension-format",
550+
choices=["json", "yaml"],
551+
default="yaml",
552+
help="Format for the generated data extension files (default %(default)s)",
553+
)
546554
args = parser.parse_args()
547555

548556
# Load config file

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

Lines changed: 30 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
@@ -38,6 +19,7 @@ def __init__(self, language, dbDir):
3819
)
3920
self.extDir = os.path.join(self.codeQlRoot, f"{self.language}/ql/lib/ext/")
4021
self.dirname = "modelconverter"
22+
# N.B. change this to .model.json to change the output format:
4123
self.modelFileExtension = ".model.yml"
4224
self.workDir = tempfile.mkdtemp()
4325

@@ -63,48 +45,38 @@ def runQuery(self, query):
6345
)
6446
return helpers.readData(self.workDir, resultBqrs)
6547

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):
48+
def merge_query_results(self, query, predicate, mergers):
7749
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]),
50+
for row in data:
51+
provenance = row[-1]
52+
namespace = row[0]
53+
target_merger = (
54+
mergers[1] if provenance.endswith("generated") else mergers[0]
55+
)
56+
target_merger.add_row(namespace, predicate, row)
57+
58+
def make_extensions(self):
59+
mergers = [
60+
helpers.ExtensionMerger(f"codeql/{self.language}-all"),
61+
helpers.ExtensionMerger(f"codeql/{self.language}-all"),
8962
]
63+
self.merge_query_results(
64+
"ExtractSummaries.ql", helpers.summaryModelPredicate, mergers
65+
)
66+
self.merge_query_results(
67+
"ExtractSources.ql", helpers.sourceModelPredicate, mergers
68+
)
69+
self.merge_query_results("ExtractSinks.ql", helpers.sinkModelPredicate, mergers)
70+
self.merge_query_results(
71+
"ExtractNeutrals.ql", helpers.neutralModelPredicate, mergers
72+
)
73+
return mergers
74+
75+
def run(self):
76+
mergers = self.make_extensions()
9077

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

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)
81+
mergers[0].save(self.extDir, self.modelFileExtension)
82+
mergers[1].save(self.extDir + "/generated", self.modelFileExtension)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 or JSON 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))
49+
50+
def json_for_row(self, row):
51+
return "[" + ", ".join(json.dumps(value) for value in row) + "]"
52+
53+
def json_for_predicate(self, predicate):
54+
return f""" {{
55+
"addsTo": {{
56+
"pack": "{self.pack}",
57+
"extensible": "{predicate}"
58+
}},
59+
"data": [
60+
{',\n '.join(self.json_for_row(row) for row in self.predicates[predicate].keys())}
61+
]
62+
}}"""
63+
64+
def write_json(self, f):
65+
# It would be preferable to use a serializer like Python's json.dumps, but it can't
66+
# pretty-print with one line per tuple.
67+
f.write("// THIS FILE IS AN AUTO-GENERATED MODELS AS DATA FILE. DO NOT EDIT.\n")
68+
f.write("{\n")
69+
f.write(' "extensions": [\n')
70+
for i, predicate in enumerate(self.predicates):
71+
if i > 0:
72+
f.write(",\n")
73+
f.write(self.json_for_predicate(predicate))
74+
f.write("\n")
75+
f.write(" ]\n")
76+
f.write("}")

0 commit comments

Comments
 (0)