Repository navigation
Expand file tree
/
Copy pathvalidate.js
More file actions
98 lines (86 loc) · 3.5 KB
/
Copy pathvalidate.js
File metadata and controls
98 lines (86 loc) · 3.5 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
const SUPPORTED_SCHEMA_MAJOR = 1;
const REQUIRED_TOP_LEVEL = [
'schema_version',
'generated_at',
'coverage_window',
'attack_families',
'indicators',
'indexes',
'sources',
];
const INDICATOR_TYPES = ['package', 'file', 'process', 'github', 'network', 'mcp'];
const VALID_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
/**
* Validate a normalized iocs.json document. Returns { ok: true } or { ok: false, errors: [...] }.
* Lightweight; not a full JSON Schema validator. Catches the schema contract violations
* that would actually break downstream consumers.
*
* @param {Object} doc
* @returns {{ok: true} | {ok: false, errors: string[]}}
*/
export function validate(doc) {
const errors = [];
for (const field of REQUIRED_TOP_LEVEL) {
if (!(field in doc)) errors.push(`missing top-level field: ${field}`);
}
if (doc.schema_version) {
const major = parseInt(String(doc.schema_version).split('.')[0], 10);
if (major !== SUPPORTED_SCHEMA_MAJOR) {
errors.push(`unsupported schema_version major: ${doc.schema_version}`);
}
}
if (doc.indicators && typeof doc.indicators === 'object') {
for (const type of INDICATOR_TYPES) {
if (!Array.isArray(doc.indicators[type])) {
errors.push(`indicators.${type} must be an array`);
}
}
for (const type of INDICATOR_TYPES) {
for (const ind of doc.indicators[type] ?? []) {
errors.push(...validateIndicator(ind, type, doc.attack_families ?? {}));
}
}
}
if (doc.attack_families && typeof doc.attack_families === 'object') {
for (const [key, fam] of Object.entries(doc.attack_families)) {
errors.push(...validateAttackFamily(key, fam));
}
}
return errors.length === 0 ? { ok: true } : { ok: false, errors };
}
function validateIndicator(ind, expectedType, attackFamilies) {
const errors = [];
const id = ind?.id ?? '(missing id)';
if (!ind.id) errors.push(`indicator missing id`);
if (ind.type !== expectedType) errors.push(`[${id}] type mismatch: got ${ind.type}, expected ${expectedType}`);
if (!VALID_SEVERITIES.includes(ind.severity)) {
errors.push(`[${id}] invalid severity: ${ind.severity}`);
}
if (!ind.attack_family) {
errors.push(`[${id}] missing attack_family`);
} else if (!attackFamilies[ind.attack_family]) {
errors.push(`[${id}] references unknown attack_family: ${ind.attack_family}`);
}
if (!ind.first_seen) errors.push(`[${id}] missing first_seen`);
if (!ind.source) errors.push(`[${id}] missing source`);
if (!ind.description) errors.push(`[${id}] missing description`);
// Required-fields rule: critical/high need remediation with at least one what_to_do step
if (ind.severity === 'critical' || ind.severity === 'high') {
if (!ind.remediation || !Array.isArray(ind.remediation.what_to_do) || ind.remediation.what_to_do.length === 0) {
errors.push(`[${id}] severity=${ind.severity} requires remediation.what_to_do with at least one step`);
}
}
return errors;
}
function validateAttackFamily(key, fam) {
const errors = [];
if (!fam.display_name) errors.push(`attack_family[${key}] missing display_name`);
if (!fam.first_observed) errors.push(`attack_family[${key}] missing first_observed`);
if (!fam.primary_external_source?.url) {
errors.push(`attack_family[${key}] missing primary_external_source.url — required`);
}
if (fam.destructive_failsafe === true && !fam.failsafe_warning) {
errors.push(`attack_family[${key}] destructive_failsafe=true requires failsafe_warning`);
}
return errors;
}