Repository navigation
Expand file tree
/
Copy pathgenerate-models.cjs
More file actions
115 lines (109 loc) · 5.02 KB
/
Copy pathgenerate-models.cjs
File metadata and controls
115 lines (109 loc) · 5.02 KB
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
const { readFileSync, writeFileSync, existsSync } = require("node:fs");
const { join, resolve } = require("node:path");
const { compile } = require("json-schema-to-typescript");
const { format } = require("prettier");
const packageRoot = resolve(__dirname, "..");
const schemas = resolve(packageRoot, "../../plugins/codex-security/schemas");
if (!existsSync(schemas)) throw new Error("Could not find the plugin schemas.");
function modelSchema(value, root = value) {
if (Array.isArray(value))
return value.map((child) => modelSchema(child, root));
if (value === null || typeof value !== "object") return value;
if (typeof value.$ref === "string") {
return modelSchema(root.$defs[value.$ref.slice("#/$defs/".length)], root);
}
return Object.fromEntries(
Object.entries(value)
.filter(([key]) => key !== "allOf" && key !== "$defs")
.map(([key, child]) => [key, modelSchema(child, root)]),
);
}
async function generate() {
const documents = [
["scan-manifest.schema.json", "ScanManifest"],
["findings.schema.json", "FindingsDocument"],
["coverage.schema.json", "CoverageDocument"],
];
const models = await Promise.all(
documents.map(async ([filename, name]) => {
// json-schema-to-typescript drops object fields when allOf uses contains or if/then.
const input = modelSchema(
JSON.parse(readFileSync(join(schemas, filename), "utf8")),
);
input.title = name;
return compile(input, name, {
bannerComment: "",
format: false,
ignoreMinAndMaxItems: true,
unknownAny: true,
});
}),
);
return format(
[
"/* Generated from the plugin JSON Schemas. Run `pnpm generate:models`. */",
...models.map((model) => model.trim()),
"export type ContractObject = Record<string, unknown>;",
'export type ScanRecord = ScanManifest["scan"];',
'export type ScanProducer = ScanRecord["producer"];',
'export type ScanTargetRecord = ScanRecord["target"];',
'export type TargetKind = ScanTargetRecord["kind"];',
'export type ScanScope = ScanRecord["scope"];',
'export type ThreatModel = NonNullable<ScanRecord["threatModel"]>;',
'export type ScanHardening = NonNullable<ScanRecord["hardening"]>;',
'export type ScanArtifact = ScanRecord["artifacts"][number];',
'export type Finding = FindingsDocument["findings"][number];',
'export type FindingIdentity = Finding["identity"];',
'export type FindingFingerprints = Finding["fingerprints"];',
'export type FindingSeverity = Finding["severity"];',
'export type SeverityLevel = FindingSeverity["level"];',
'export type FindingConfidence = Finding["confidence"];',
'export type ConfidenceLevel = FindingConfidence["level"];',
'export type FindingTaxonomy = Finding["taxonomy"];',
'export type FindingLocation = Finding["locations"][number];',
'export type FindingWriteup = NonNullable<Finding["writeup"]>;',
'export type FindingCodeEvidence = NonNullable<Finding["codeEvidence"]>[number];',
'export type FindingRootCause = Extract<Finding["rootCause"], object>;',
'export type FindingValidation = NonNullable<Finding["validation"]>;',
'export type FindingAttackPath = NonNullable<Finding["attackPath"]>;',
'export type AttackPathDataflow = Extract<NonNullable<FindingAttackPath["dataFlow"]>, object>;',
'export type AttackPathReachability = Extract<NonNullable<FindingAttackPath["reachability"]>, object>;',
'export type FindingProvenance = Finding["provenance"];',
'export type CoverageMode = CoverageDocument["mode"];',
'export type InventoryStrategy = CoverageDocument["inventoryStrategy"];',
'export type CoverageSurface = CoverageDocument["surfaces"][number];',
'export type SurfaceDisposition = CoverageSurface["disposition"];',
'export type ExplicitExclusion = CoverageDocument["explicitExclusions"][number];',
'export type DeferredCoverage = CoverageDocument["deferred"][number];',
'export type CoverageOpenQuestion = NonNullable<CoverageDocument["openQuestions"]>[number];',
].join("\n\n"),
{ parser: "typescript", printWidth: 80 },
);
}
generate().then((models) => {
const pluginSource = "../../plugins/codex-security/mcp-app/src";
const shared = [
"workbench-arguments",
"executable-path",
"contract-values",
"windows-path",
].map((name) => [
`src/${name}.ts`,
`/* Generated from plugin ${name}.ts. Run \`pnpm generate:models\`. */\n` +
readFileSync(join(packageRoot, pluginSource, `${name}.ts`), "utf8"),
]);
const outputs = [
[`${pluginSource}/models.ts`, models],
["src/models.ts", models],
...shared,
];
for (const [name, content] of outputs) {
const output = join(packageRoot, name);
if (process.argv.includes("--check")) {
if (readFileSync(output, "utf8").replaceAll("\r\n", "\n") !== content) {
console.error(`${name} is out of date. Run \`pnpm generate:models\`.`);
process.exitCode = 1;
}
} else writeFileSync(output, content);
}
});