@adversarylabs/sdk 0.1.43 → 0.1.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/candidate-recovery.d.ts +1 -1
- package/dist/candidate-recovery.d.ts.map +1 -1
- package/dist/candidate-recovery.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +108 -5
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +26 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +127 -2
- package/dist/model.js.map +1 -1
- package/dist/repository-model.d.ts.map +1 -1
- package/dist/repository-model.js +293 -21
- package/dist/repository-model.js.map +1 -1
- package/dist/review-sources.d.ts +34 -0
- package/dist/review-sources.d.ts.map +1 -0
- package/dist/review-sources.js +195 -0
- package/dist/review-sources.js.map +1 -0
- package/package.json +1 -1
- package/schemas/adversary.review.v1.schema.json +76 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export const REVIEW_SOURCE_STATUSES = [
|
|
2
|
+
"passed",
|
|
3
|
+
"findings",
|
|
4
|
+
"error",
|
|
5
|
+
"partial",
|
|
6
|
+
"unavailable",
|
|
7
|
+
];
|
|
8
|
+
const sourceIdPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
9
|
+
const secretNames = "password|passwd|secret|token|access[_-]?token|access[_-]?key|api[_-]?key|authorization|private[_-]?key";
|
|
10
|
+
const credentialName = `(?:[a-z0-9]+[_-])*(?:${secretNames})`;
|
|
11
|
+
const secretKey = new RegExp(`^${credentialName}$`, "i");
|
|
12
|
+
const secretAssignment = new RegExp(`((?<![\\w-])["']?${credentialName}["']?\\s*[=:]\\s*)(?:"(?:\\\\.|[^"\\\\])*"|'(?:\\\\.|[^'\\\\])*'|(?:Basic|Bearer)\\s+[^\\s,;]+|[^\\s,;]+)`, "gi");
|
|
13
|
+
export class ReviewSourceValidationError extends Error {
|
|
14
|
+
field;
|
|
15
|
+
code = "invalid_review_source";
|
|
16
|
+
sourceId;
|
|
17
|
+
constructor(field, detail, sourceId) {
|
|
18
|
+
const safeId = typeof sourceId === "string" && sourceIdPattern.test(sourceId) ? sourceId : undefined;
|
|
19
|
+
super(`${field}${safeId ? ` (source ${safeId})` : ""}: ${detail}`);
|
|
20
|
+
this.field = field;
|
|
21
|
+
this.sourceId = safeId;
|
|
22
|
+
this.name = "ReviewSourceValidationError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class ReviewSourceCapacityError extends ReviewSourceValidationError {
|
|
26
|
+
code = "review_source_capacity_exceeded";
|
|
27
|
+
constructor(sourceId) {
|
|
28
|
+
super("ctx.review.source", "at most 32 sources are allowed", sourceId);
|
|
29
|
+
this.name = "ReviewSourceCapacityError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function parseReviewSourceUrl(value, field, sourceId) {
|
|
33
|
+
if (typeof value !== "string")
|
|
34
|
+
throw new ReviewSourceValidationError(field, "must be an HTTP(S) URL", sourceId);
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(value);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new ReviewSourceValidationError(field, "must be a valid HTTP(S) URL", sourceId);
|
|
41
|
+
}
|
|
42
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
43
|
+
throw new ReviewSourceValidationError(field, "must be an HTTP(S) URL without credentials", sourceId);
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
function redact(text) {
|
|
47
|
+
// Hide assignment values first so a Bearer mention cannot consume their keys.
|
|
48
|
+
return text
|
|
49
|
+
.replace(secretAssignment, '$1"[REDACTED]"')
|
|
50
|
+
.replace(/(Bearer\s+)[A-Za-z0-9._~+\/-]+=*(?![A-Za-z0-9._~+\/=-])/gi, "$1[REDACTED]");
|
|
51
|
+
}
|
|
52
|
+
/** JSON Schema limits characters; the wire validator also enforces this shared UTF-8 budget. */
|
|
53
|
+
export function validateReviewSourceBudget(source, label) {
|
|
54
|
+
let data;
|
|
55
|
+
try {
|
|
56
|
+
data = JSON.stringify(source.data) ?? "";
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new ReviewSourceValidationError(`${label}.data`, "must be JSON-serializable", source.id);
|
|
60
|
+
}
|
|
61
|
+
if (Buffer.byteLength(source.output ?? "") + Buffer.byteLength(data) > 32768)
|
|
62
|
+
throw new ReviewSourceValidationError(`${label}.data/output`, "combined UTF-8 payload exceeds 32768 bytes", source.id);
|
|
63
|
+
}
|
|
64
|
+
/** Decode valid runs only; lone percent signs and invalid UTF-8 remain legal URL text. */
|
|
65
|
+
function redactUrlComponent(encoded) {
|
|
66
|
+
const decoded = encoded.replace(/(?:%[0-9a-f]{2})+/gi, (run) => {
|
|
67
|
+
try {
|
|
68
|
+
return decodeURIComponent(run);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return run;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const safe = redact(decoded);
|
|
75
|
+
// Re-encode changed components so decoded separators cannot alter URL structure.
|
|
76
|
+
return safe === decoded ? encoded : encodeURIComponent(safe);
|
|
77
|
+
}
|
|
78
|
+
export function validateSourceIds(value, field) {
|
|
79
|
+
if (value === undefined)
|
|
80
|
+
return;
|
|
81
|
+
if (!Array.isArray(value) ||
|
|
82
|
+
value.some((id) => typeof id !== "string" || !sourceIdPattern.test(id)))
|
|
83
|
+
throw new ReviewSourceValidationError(field, "must be an array of valid source IDs");
|
|
84
|
+
}
|
|
85
|
+
/** Producers must remove domain-specific secrets before reporting. This is defense in depth. */
|
|
86
|
+
export function normalizeReviewSource(input, label = "reviewSource") {
|
|
87
|
+
const safeId = typeof input?.id === "string" && sourceIdPattern.test(input.id) ? input.id : undefined;
|
|
88
|
+
const fail = (field, detail) => {
|
|
89
|
+
throw new ReviewSourceValidationError(`${label}.${field}`, detail, safeId);
|
|
90
|
+
};
|
|
91
|
+
if (!safeId)
|
|
92
|
+
fail("id", "must be a valid source ID");
|
|
93
|
+
if (!REVIEW_SOURCE_STATUSES.some((status) => status === input.status))
|
|
94
|
+
fail("status", "must be a supported source status");
|
|
95
|
+
if (input.truncated !== undefined && typeof input.truncated !== "boolean")
|
|
96
|
+
fail("truncated", "must be a boolean");
|
|
97
|
+
let truncated = input.truncated ?? false;
|
|
98
|
+
const bound = (value, max, field) => {
|
|
99
|
+
if (typeof value !== "string")
|
|
100
|
+
fail(field, "must be a string");
|
|
101
|
+
const safe = field === "output" ? redact(value) : value;
|
|
102
|
+
const bytes = Buffer.from(safe);
|
|
103
|
+
if (bytes.length <= max)
|
|
104
|
+
return safe;
|
|
105
|
+
truncated = true;
|
|
106
|
+
return new TextDecoder("utf-8", { fatal: false })
|
|
107
|
+
.decode(bytes.subarray(0, max))
|
|
108
|
+
.replace(/\uFFFD$/, "");
|
|
109
|
+
};
|
|
110
|
+
const required = (value, max, field) => {
|
|
111
|
+
const text = bound(value, max, field).trim();
|
|
112
|
+
if (!text)
|
|
113
|
+
fail(field, "must be non-empty");
|
|
114
|
+
return text;
|
|
115
|
+
};
|
|
116
|
+
const result = {
|
|
117
|
+
id: input.id,
|
|
118
|
+
kind: required(input.kind, 80, "kind"),
|
|
119
|
+
title: required(input.title, 200, "title"),
|
|
120
|
+
status: input.status,
|
|
121
|
+
summary: required(input.summary, 2000, "summary"),
|
|
122
|
+
};
|
|
123
|
+
if (input.output !== undefined)
|
|
124
|
+
result.output = bound(input.output, 32768, "output");
|
|
125
|
+
if (input.data !== undefined) {
|
|
126
|
+
if (!input.data || Array.isArray(input.data) || typeof input.data !== "object")
|
|
127
|
+
fail("data", "must be an object");
|
|
128
|
+
let serialized = "";
|
|
129
|
+
try {
|
|
130
|
+
serialized = JSON.stringify(input.data, (key, value) => secretKey.test(key) ? "[REDACTED]" : typeof value === "string" ? redact(value) : value);
|
|
131
|
+
const parsed = JSON.parse(serialized);
|
|
132
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object")
|
|
133
|
+
fail("data", "must serialize to an object");
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof ReviewSourceValidationError)
|
|
137
|
+
throw error;
|
|
138
|
+
fail("data", "must be a JSON-serializable object (received object)");
|
|
139
|
+
}
|
|
140
|
+
const dataBytes = Buffer.byteLength(serialized);
|
|
141
|
+
if (dataBytes > 32768) {
|
|
142
|
+
truncated = true;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
result.data = JSON.parse(serialized);
|
|
146
|
+
if (result.output !== undefined)
|
|
147
|
+
result.output = bound(result.output, 32768 - dataBytes, "output");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (input.reviewer !== undefined)
|
|
151
|
+
result.reviewer = bound(input.reviewer, 500, "reviewer");
|
|
152
|
+
if (input.version !== undefined)
|
|
153
|
+
result.version = bound(input.version, 200, "version");
|
|
154
|
+
if (input.headSha !== undefined)
|
|
155
|
+
result.headSha = bound(input.headSha, 100, "headSha");
|
|
156
|
+
if (input.scope !== undefined) {
|
|
157
|
+
if (!Array.isArray(input.scope))
|
|
158
|
+
fail("scope", "must be an array of strings");
|
|
159
|
+
const invalidIndex = input.scope.findIndex((item) => typeof item !== "string");
|
|
160
|
+
if (invalidIndex !== -1)
|
|
161
|
+
fail(`scope[${invalidIndex}]`, "must be a string");
|
|
162
|
+
if (input.scope.length > 100)
|
|
163
|
+
truncated = true;
|
|
164
|
+
result.scope = input.scope
|
|
165
|
+
.slice(0, 100)
|
|
166
|
+
.map((item, index) => bound(item, 500, `scope[${index}]`));
|
|
167
|
+
}
|
|
168
|
+
if (input.url !== undefined) {
|
|
169
|
+
if (typeof input.url !== "string")
|
|
170
|
+
fail("url", "must be an HTTP(S) URL without credentials");
|
|
171
|
+
try {
|
|
172
|
+
const url = parseReviewSourceUrl(input.url, `${label}.url`, safeId);
|
|
173
|
+
const params = new URLSearchParams();
|
|
174
|
+
for (const [key, value] of url.searchParams) {
|
|
175
|
+
params.append(key, secretKey.test(key) ? "[REDACTED]" : redact(value));
|
|
176
|
+
}
|
|
177
|
+
url.search = params.toString();
|
|
178
|
+
url.pathname = url.pathname.split("/").map(redactUrlComponent).join("/");
|
|
179
|
+
if (url.hash)
|
|
180
|
+
url.hash = `#${redactUrlComponent(url.hash.slice(1))}`;
|
|
181
|
+
result.url = url.toString();
|
|
182
|
+
if (result.url.length > 2000)
|
|
183
|
+
fail("url", "must be at most 2000 characters");
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (error instanceof ReviewSourceValidationError)
|
|
187
|
+
throw error;
|
|
188
|
+
fail("url", "must be a valid HTTP(S) URL without credentials, at most 2000 characters");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (truncated)
|
|
192
|
+
result.truncated = true;
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=review-sources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-sources.js","sourceRoot":"","sources":["../src/review-sources.ts"],"names":[],"mappings":"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","sourcesContent":["export const REVIEW_SOURCE_STATUSES = [\n \"passed\",\n \"findings\",\n \"error\",\n \"partial\",\n \"unavailable\",\n] as const;\n\n/** Evidence reported by a scanner or artifact reader. Output is untrusted text, never HTML. */\nexport interface ReviewSource {\n id: string;\n kind: string;\n title: string;\n status: (typeof REVIEW_SOURCE_STATUSES)[number];\n summary: string;\n output?: string;\n data?: Record<string, unknown>;\n truncated?: boolean;\n version?: string;\n reviewer?: string;\n scope?: string[];\n headSha?: string;\n url?: string;\n}\n\nconst sourceIdPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;\nconst secretNames =\n \"password|passwd|secret|token|access[_-]?token|access[_-]?key|api[_-]?key|authorization|private[_-]?key\";\nconst credentialName = `(?:[a-z0-9]+[_-])*(?:${secretNames})`;\nconst secretKey = new RegExp(`^${credentialName}$`, \"i\");\nconst secretAssignment = new RegExp(\n `((?<![\\\\w-])[\"']?${credentialName}[\"']?\\\\s*[=:]\\\\s*)(?:\"(?:\\\\\\\\.|[^\"\\\\\\\\])*\"|'(?:\\\\\\\\.|[^'\\\\\\\\])*'|(?:Basic|Bearer)\\\\s+[^\\\\s,;]+|[^\\\\s,;]+)`,\n \"gi\",\n);\n\nexport class ReviewSourceValidationError extends Error {\n readonly code: string = \"invalid_review_source\";\n readonly sourceId?: string;\n constructor(\n readonly field: string,\n detail: string,\n sourceId?: string,\n ) {\n const safeId =\n typeof sourceId === \"string\" && sourceIdPattern.test(sourceId) ? sourceId : undefined;\n super(`${field}${safeId ? ` (source ${safeId})` : \"\"}: ${detail}`);\n this.sourceId = safeId;\n this.name = \"ReviewSourceValidationError\";\n }\n}\n\nexport class ReviewSourceCapacityError extends ReviewSourceValidationError {\n override readonly code = \"review_source_capacity_exceeded\";\n constructor(sourceId: string) {\n super(\"ctx.review.source\", \"at most 32 sources are allowed\", sourceId);\n this.name = \"ReviewSourceCapacityError\";\n }\n}\n\nexport function parseReviewSourceUrl(value: unknown, field: string, sourceId?: string): URL {\n if (typeof value !== \"string\")\n throw new ReviewSourceValidationError(field, \"must be an HTTP(S) URL\", sourceId);\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new ReviewSourceValidationError(field, \"must be a valid HTTP(S) URL\", sourceId);\n }\n if (![\"https:\", \"http:\"].includes(url.protocol) || url.username || url.password)\n throw new ReviewSourceValidationError(\n field,\n \"must be an HTTP(S) URL without credentials\",\n sourceId,\n );\n return url;\n}\n\nfunction redact(text: string): string {\n // Hide assignment values first so a Bearer mention cannot consume their keys.\n return text\n .replace(secretAssignment, '$1\"[REDACTED]\"')\n .replace(/(Bearer\\s+)[A-Za-z0-9._~+\\/-]+=*(?![A-Za-z0-9._~+\\/=-])/gi, \"$1[REDACTED]\");\n}\n\n/** JSON Schema limits characters; the wire validator also enforces this shared UTF-8 budget. */\nexport function validateReviewSourceBudget(source: ReviewSource, label: string): void {\n let data: string;\n try {\n data = JSON.stringify(source.data) ?? \"\";\n } catch {\n throw new ReviewSourceValidationError(`${label}.data`, \"must be JSON-serializable\", source.id);\n }\n if (Buffer.byteLength(source.output ?? \"\") + Buffer.byteLength(data) > 32768)\n throw new ReviewSourceValidationError(\n `${label}.data/output`,\n \"combined UTF-8 payload exceeds 32768 bytes\",\n source.id,\n );\n}\n\n/** Decode valid runs only; lone percent signs and invalid UTF-8 remain legal URL text. */\nfunction redactUrlComponent(encoded: string): string {\n const decoded = encoded.replace(/(?:%[0-9a-f]{2})+/gi, (run) => {\n try {\n return decodeURIComponent(run);\n } catch {\n return run;\n }\n });\n const safe = redact(decoded);\n // Re-encode changed components so decoded separators cannot alter URL structure.\n return safe === decoded ? encoded : encodeURIComponent(safe);\n}\n\nexport function validateSourceIds(\n value: unknown,\n field: string,\n): asserts value is string[] | undefined {\n if (value === undefined) return;\n if (\n !Array.isArray(value) ||\n value.some((id) => typeof id !== \"string\" || !sourceIdPattern.test(id))\n )\n throw new ReviewSourceValidationError(field, \"must be an array of valid source IDs\");\n}\n\n/** Producers must remove domain-specific secrets before reporting. This is defense in depth. */\nexport function normalizeReviewSource(input: ReviewSource, label = \"reviewSource\"): ReviewSource {\n const safeId =\n typeof input?.id === \"string\" && sourceIdPattern.test(input.id) ? input.id : undefined;\n const fail = (field: string, detail: string): never => {\n throw new ReviewSourceValidationError(`${label}.${field}`, detail, safeId);\n };\n if (!safeId) fail(\"id\", \"must be a valid source ID\");\n if (!REVIEW_SOURCE_STATUSES.some((status) => status === input.status))\n fail(\"status\", \"must be a supported source status\");\n if (input.truncated !== undefined && typeof input.truncated !== \"boolean\")\n fail(\"truncated\", \"must be a boolean\");\n let truncated = input.truncated ?? false;\n const bound = (value: string, max: number, field: string) => {\n if (typeof value !== \"string\") fail(field, \"must be a string\");\n const safe = field === \"output\" ? redact(value) : value;\n const bytes = Buffer.from(safe);\n if (bytes.length <= max) return safe;\n truncated = true;\n return new TextDecoder(\"utf-8\", { fatal: false })\n .decode(bytes.subarray(0, max))\n .replace(/\\uFFFD$/, \"\");\n };\n const required = (value: string, max: number, field: string) => {\n const text = bound(value, max, field).trim();\n if (!text) fail(field, \"must be non-empty\");\n return text;\n };\n const result: ReviewSource = {\n id: input.id,\n kind: required(input.kind, 80, \"kind\"),\n title: required(input.title, 200, \"title\"),\n status: input.status,\n summary: required(input.summary, 2000, \"summary\"),\n };\n if (input.output !== undefined) result.output = bound(input.output, 32768, \"output\");\n if (input.data !== undefined) {\n if (!input.data || Array.isArray(input.data) || typeof input.data !== \"object\")\n fail(\"data\", \"must be an object\");\n let serialized = \"\";\n try {\n serialized = JSON.stringify(input.data, (key, value) =>\n secretKey.test(key) ? \"[REDACTED]\" : typeof value === \"string\" ? redact(value) : value,\n );\n const parsed: unknown = JSON.parse(serialized);\n if (!parsed || Array.isArray(parsed) || typeof parsed !== \"object\")\n fail(\"data\", \"must serialize to an object\");\n } catch (error) {\n if (error instanceof ReviewSourceValidationError) throw error;\n fail(\"data\", \"must be a JSON-serializable object (received object)\");\n }\n const dataBytes = Buffer.byteLength(serialized);\n if (dataBytes > 32768) {\n truncated = true;\n } else {\n result.data = JSON.parse(serialized);\n if (result.output !== undefined)\n result.output = bound(result.output, 32768 - dataBytes, \"output\");\n }\n }\n if (input.reviewer !== undefined) result.reviewer = bound(input.reviewer, 500, \"reviewer\");\n if (input.version !== undefined) result.version = bound(input.version, 200, \"version\");\n if (input.headSha !== undefined) result.headSha = bound(input.headSha, 100, \"headSha\");\n if (input.scope !== undefined) {\n if (!Array.isArray(input.scope)) fail(\"scope\", \"must be an array of strings\");\n const invalidIndex = input.scope.findIndex((item) => typeof item !== \"string\");\n if (invalidIndex !== -1) fail(`scope[${invalidIndex}]`, \"must be a string\");\n if (input.scope.length > 100) truncated = true;\n result.scope = input.scope\n .slice(0, 100)\n .map((item, index) => bound(item, 500, `scope[${index}]`));\n }\n if (input.url !== undefined) {\n if (typeof input.url !== \"string\") fail(\"url\", \"must be an HTTP(S) URL without credentials\");\n try {\n const url = parseReviewSourceUrl(input.url, `${label}.url`, safeId);\n const params = new URLSearchParams();\n for (const [key, value] of url.searchParams) {\n params.append(key, secretKey.test(key) ? \"[REDACTED]\" : redact(value));\n }\n url.search = params.toString();\n url.pathname = url.pathname.split(\"/\").map(redactUrlComponent).join(\"/\");\n if (url.hash) url.hash = `#${redactUrlComponent(url.hash.slice(1))}`;\n result.url = url.toString();\n if (result.url.length > 2000) fail(\"url\", \"must be at most 2000 characters\");\n } catch (error) {\n if (error instanceof ReviewSourceValidationError) throw error;\n fail(\"url\", \"must be a valid HTTP(S) URL without credentials, at most 2000 characters\");\n }\n }\n if (truncated) result.truncated = true;\n return result;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -10,6 +10,71 @@
|
|
|
10
10
|
"result": { "$ref": "#/$defs/reviewResult" }
|
|
11
11
|
},
|
|
12
12
|
"$defs": {
|
|
13
|
+
"reviewSource": {
|
|
14
|
+
"description": "The SDK additionally enforces a combined 32768-byte UTF-8 budget for output and JSON-serialized data; JSON Schema maxLength limits characters only.",
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"required": ["id", "kind", "title", "status", "summary"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"id": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
|
|
22
|
+
},
|
|
23
|
+
"kind": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"minLength": 1,
|
|
26
|
+
"maxLength": 80
|
|
27
|
+
},
|
|
28
|
+
"title": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"minLength": 1,
|
|
31
|
+
"maxLength": 200
|
|
32
|
+
},
|
|
33
|
+
"status": {
|
|
34
|
+
"enum": ["passed", "findings", "error", "partial", "unavailable"]
|
|
35
|
+
},
|
|
36
|
+
"summary": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"minLength": 1,
|
|
39
|
+
"maxLength": 2000
|
|
40
|
+
},
|
|
41
|
+
"output": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"maxLength": 32768
|
|
44
|
+
},
|
|
45
|
+
"data": {
|
|
46
|
+
"type": "object"
|
|
47
|
+
},
|
|
48
|
+
"truncated": {
|
|
49
|
+
"type": "boolean"
|
|
50
|
+
},
|
|
51
|
+
"version": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"maxLength": 200
|
|
54
|
+
},
|
|
55
|
+
"scope": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"maxItems": 100,
|
|
58
|
+
"items": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"maxLength": 500
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"headSha": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"maxLength": 100
|
|
66
|
+
},
|
|
67
|
+
"url": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"pattern": "^https?://",
|
|
70
|
+
"maxLength": 2000
|
|
71
|
+
},
|
|
72
|
+
"reviewer": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"maxLength": 500
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
13
78
|
"metadata": { "type": "object" },
|
|
14
79
|
"adversary": {
|
|
15
80
|
"type": "object",
|
|
@@ -57,6 +122,12 @@
|
|
|
57
122
|
"additionalProperties": false,
|
|
58
123
|
"required": ["id", "title", "category", "severity", "confidence", "summary", "evidence"],
|
|
59
124
|
"properties": {
|
|
125
|
+
"sourceIds": {
|
|
126
|
+
"type": "array",
|
|
127
|
+
"maxItems": 256,
|
|
128
|
+
"uniqueItems": true,
|
|
129
|
+
"items": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" }
|
|
130
|
+
},
|
|
60
131
|
"id": { "type": "string", "minLength": 1, "pattern": "\\S" },
|
|
61
132
|
"ruleId": { "type": "string" },
|
|
62
133
|
"groupKey": { "type": "string" },
|
|
@@ -86,6 +157,11 @@
|
|
|
86
157
|
"additionalProperties": false,
|
|
87
158
|
"required": ["adversary", "target", "positives", "observations", "findings", "suppressed"],
|
|
88
159
|
"properties": {
|
|
160
|
+
"sources": {
|
|
161
|
+
"type": "array",
|
|
162
|
+
"maxItems": 256,
|
|
163
|
+
"items": { "$ref": "#/$defs/reviewSource" }
|
|
164
|
+
},
|
|
89
165
|
"adversary": { "$ref": "#/$defs/adversary" },
|
|
90
166
|
"target": { "$ref": "#/$defs/target" },
|
|
91
167
|
"assessment": {
|