llm-checker 3.2.1 → 3.2.3
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 +93 -8
- package/bin/enhanced_cli.js +447 -0
- package/package.json +7 -1
- package/src/index.js +84 -20
- package/src/models/deterministic-selector.js +406 -22
- package/src/models/intelligent-selector.js +89 -4
- package/src/policy/audit-reporter.js +420 -0
- package/src/policy/cli-policy.js +403 -0
- package/src/policy/policy-engine.js +497 -0
- package/src/policy/policy-manager.js +324 -0
- package/src/provenance/model-provenance.js +176 -0
|
@@ -4,14 +4,25 @@
|
|
|
4
4
|
* Provides smart recommendations based on use case, hardware, and preferences
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
7
9
|
const ScoringEngine = require('./scoring-engine');
|
|
8
10
|
const UnifiedDetector = require('../hardware/unified-detector');
|
|
11
|
+
const PolicyManager = require('../policy/policy-manager');
|
|
12
|
+
const PolicyEngine = require('../policy/policy-engine');
|
|
13
|
+
|
|
14
|
+
function isPlainObject(value) {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
9
17
|
|
|
10
18
|
class IntelligentSelector {
|
|
11
19
|
constructor(options = {}) {
|
|
12
20
|
this.scoring = new ScoringEngine(options.scoring || {});
|
|
13
21
|
this.detector = options.detector || new UnifiedDetector();
|
|
14
22
|
this.database = options.database || null;
|
|
23
|
+
this.policyManager = options.policyManager || new PolicyManager();
|
|
24
|
+
this.policyEngine = options.policyEngine || null;
|
|
25
|
+
this.defaultPolicyFile = options.policyFile || 'policy.yaml';
|
|
15
26
|
|
|
16
27
|
// Default preferences
|
|
17
28
|
this.defaults = {
|
|
@@ -25,6 +36,7 @@ class IntelligentSelector {
|
|
|
25
36
|
excludeFamilies: [],
|
|
26
37
|
includeVision: false,
|
|
27
38
|
includeEmbeddings: false,
|
|
39
|
+
policyFile: this.defaultPolicyFile,
|
|
28
40
|
limit: 10
|
|
29
41
|
};
|
|
30
42
|
}
|
|
@@ -61,25 +73,35 @@ class IntelligentSelector {
|
|
|
61
73
|
headroom: opts.headroom || 2
|
|
62
74
|
});
|
|
63
75
|
|
|
76
|
+
const policyEngine = this.resolvePolicyEngine(opts);
|
|
77
|
+
const scoredWithPolicy = policyEngine.evaluateScoredVariants(
|
|
78
|
+
scored,
|
|
79
|
+
this.buildPolicyContext(hardware, opts)
|
|
80
|
+
);
|
|
81
|
+
|
|
64
82
|
// Categorize scores
|
|
65
|
-
const categories = this.scoring.categorizeScores(
|
|
83
|
+
const categories = this.scoring.categorizeScores(scoredWithPolicy);
|
|
66
84
|
|
|
67
85
|
// Get top picks
|
|
68
|
-
const topPicks = this.selectTopPicks(
|
|
86
|
+
const topPicks = this.selectTopPicks(scoredWithPolicy, opts);
|
|
69
87
|
|
|
70
88
|
// Generate insights
|
|
71
|
-
const insights = this.generateInsights(
|
|
89
|
+
const insights = this.generateInsights(scoredWithPolicy, hardware, opts);
|
|
72
90
|
|
|
73
91
|
return {
|
|
74
92
|
topPicks,
|
|
75
93
|
categories,
|
|
76
|
-
all:
|
|
94
|
+
all: scoredWithPolicy.slice(0, opts.limit),
|
|
77
95
|
hardware: {
|
|
78
96
|
description: this.detector.getHardwareDescription(),
|
|
79
97
|
tier: this.detector.getHardwareTier(),
|
|
80
98
|
maxSize: this.detector.getMaxModelSize(),
|
|
81
99
|
backend: hardware.summary.bestBackend
|
|
82
100
|
},
|
|
101
|
+
policy: {
|
|
102
|
+
mode: policyEngine.getMode(),
|
|
103
|
+
active: policyEngine.hasActiveRules()
|
|
104
|
+
},
|
|
83
105
|
insights,
|
|
84
106
|
meta: {
|
|
85
107
|
totalCandidates: variants.length,
|
|
@@ -89,6 +111,69 @@ class IntelligentSelector {
|
|
|
89
111
|
};
|
|
90
112
|
}
|
|
91
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Resolve policy engine from explicit options, in-memory policy, or policy file.
|
|
116
|
+
*/
|
|
117
|
+
resolvePolicyEngine(opts = {}) {
|
|
118
|
+
const explicitEngine = opts.policyEngine || this.policyEngine;
|
|
119
|
+
if (
|
|
120
|
+
explicitEngine &&
|
|
121
|
+
typeof explicitEngine.evaluateScoredVariants === 'function' &&
|
|
122
|
+
typeof explicitEngine.getMode === 'function'
|
|
123
|
+
) {
|
|
124
|
+
return explicitEngine;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isPlainObject(opts.policy)) {
|
|
128
|
+
return new PolicyEngine(opts.policy);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const policyFile = opts.policyFile || this.defaultPolicyFile;
|
|
132
|
+
if (!policyFile) {
|
|
133
|
+
return new PolicyEngine(null);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const policyPath = path.isAbsolute(policyFile)
|
|
137
|
+
? policyFile
|
|
138
|
+
: path.resolve(process.cwd(), policyFile);
|
|
139
|
+
|
|
140
|
+
if (!fs.existsSync(policyPath)) {
|
|
141
|
+
return new PolicyEngine(null);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const validation = this.policyManager.validatePolicyFile(policyPath);
|
|
145
|
+
if (!validation.valid) {
|
|
146
|
+
const details = validation.errors
|
|
147
|
+
.map((error) => `${error.path}: ${error.message}`)
|
|
148
|
+
.join('; ');
|
|
149
|
+
throw new Error(`Invalid policy file at ${policyPath}. ${details}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return new PolicyEngine(validation.policy);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Build runtime context for policy checks.
|
|
157
|
+
*/
|
|
158
|
+
buildPolicyContext(hardware, opts = {}) {
|
|
159
|
+
const summary = hardware?.summary || {};
|
|
160
|
+
const systemRAM = typeof summary.systemRAM === 'number' ? summary.systemRAM : null;
|
|
161
|
+
|
|
162
|
+
const context = {
|
|
163
|
+
backend: summary.bestBackend || null,
|
|
164
|
+
runtimeBackend: summary.bestBackend || null,
|
|
165
|
+
ramGB: systemRAM,
|
|
166
|
+
totalRamGB: systemRAM,
|
|
167
|
+
hardware
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (typeof opts.isLocal === 'boolean') {
|
|
171
|
+
context.isLocal = opts.isLocal;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return context;
|
|
175
|
+
}
|
|
176
|
+
|
|
92
177
|
/**
|
|
93
178
|
* Apply filters to variant list
|
|
94
179
|
*/
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
const SEVERITY_BY_CODE = {
|
|
4
|
+
MODEL_DENIED: 'high',
|
|
5
|
+
MODEL_NOT_ALLOWED: 'high',
|
|
6
|
+
BACKEND_NOT_ALLOWED: 'high',
|
|
7
|
+
MODEL_NOT_LOCAL: 'high',
|
|
8
|
+
LICENSE_NOT_APPROVED: 'high',
|
|
9
|
+
LICENSE_MISSING: 'high',
|
|
10
|
+
MODEL_TOO_LARGE: 'medium',
|
|
11
|
+
MODEL_TOO_MANY_PARAMS: 'medium',
|
|
12
|
+
INSUFFICIENT_RAM: 'medium',
|
|
13
|
+
QUANTIZATION_NOT_ALLOWED: 'medium',
|
|
14
|
+
MODEL_SIZE_UNKNOWN: 'low',
|
|
15
|
+
MODEL_PARAMS_UNKNOWN: 'low',
|
|
16
|
+
QUANTIZATION_UNKNOWN: 'low',
|
|
17
|
+
BACKEND_UNKNOWN: 'low',
|
|
18
|
+
RAM_UNKNOWN: 'low'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const RECOMMENDATIONS_BY_CODE = {
|
|
22
|
+
MODEL_DENIED: 'Select a model that is not in the deny list or update policy approval scope.',
|
|
23
|
+
MODEL_NOT_ALLOWED: 'Use an allowlisted model identifier/tag or extend rules.models.allow.',
|
|
24
|
+
BACKEND_NOT_ALLOWED: 'Switch to an approved runtime backend or update rules.runtime.required_backends.',
|
|
25
|
+
MODEL_NOT_LOCAL: 'Use a local model/runtime path or disable rules.runtime.local_only.',
|
|
26
|
+
LICENSE_NOT_APPROVED: 'Use a model with an approved license or update rules.compliance.approved_licenses.',
|
|
27
|
+
LICENSE_MISSING: 'Populate license metadata for this model before production use.',
|
|
28
|
+
MODEL_TOO_LARGE: 'Select a smaller model or increase rules.models.max_size_gb.',
|
|
29
|
+
MODEL_TOO_MANY_PARAMS: 'Select a model with fewer parameters or increase rules.models.max_params_b.',
|
|
30
|
+
INSUFFICIENT_RAM: 'Use a smaller model/quantization or increase available system memory.',
|
|
31
|
+
QUANTIZATION_NOT_ALLOWED: 'Switch to an approved quantization in rules.models.allowed_quantizations.',
|
|
32
|
+
MODEL_SIZE_UNKNOWN: 'Add model size metadata (size_gb/size) so policy can evaluate it deterministically.',
|
|
33
|
+
MODEL_PARAMS_UNKNOWN: 'Add parameter metadata (params_b) to model metadata.',
|
|
34
|
+
QUANTIZATION_UNKNOWN: 'Add quantization metadata (quant/quantization) to model metadata.',
|
|
35
|
+
BACKEND_UNKNOWN: 'Provide runtime backend context in policy evaluation inputs.',
|
|
36
|
+
RAM_UNKNOWN: 'Provide system RAM metadata in policy evaluation context.'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function normalizeValue(value, fallback = 'unknown') {
|
|
40
|
+
if (value === undefined || value === null) return fallback;
|
|
41
|
+
const text = String(value).trim();
|
|
42
|
+
return text.length > 0 ? text : fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function csvValue(value) {
|
|
46
|
+
const text = String(value ?? '');
|
|
47
|
+
if (text.includes(',') || text.includes('"') || text.includes('\n')) {
|
|
48
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
49
|
+
}
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function deterministicRuleId(code, path) {
|
|
54
|
+
const input = `${normalizeValue(code)}|${normalizeValue(path)}`;
|
|
55
|
+
const digest = crypto.createHash('sha1').update(input).digest('hex').slice(0, 12).toUpperCase();
|
|
56
|
+
return `LLMCHECK-${digest}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getSeverityForCode(code) {
|
|
60
|
+
return SEVERITY_BY_CODE[code] || 'medium';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getRecommendationForCode(code) {
|
|
64
|
+
return RECOMMENDATIONS_BY_CODE[code] || 'Review policy rule and model metadata to remediate this violation.';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sarifLevelFromSeverity(severity) {
|
|
68
|
+
switch (severity) {
|
|
69
|
+
case 'critical':
|
|
70
|
+
case 'high':
|
|
71
|
+
return 'error';
|
|
72
|
+
case 'medium':
|
|
73
|
+
return 'warning';
|
|
74
|
+
default:
|
|
75
|
+
return 'note';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toFindingRecord(entry, generatedAt) {
|
|
80
|
+
const violation = entry?.violation || {};
|
|
81
|
+
const code = normalizeValue(violation.code, 'UNKNOWN');
|
|
82
|
+
const rulePath = normalizeValue(violation.path, 'policy');
|
|
83
|
+
const severity = getSeverityForCode(code);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
generated_at: generatedAt,
|
|
87
|
+
status: normalizeValue(entry?.status, 'active'),
|
|
88
|
+
model_identifier: normalizeValue(entry?.model_identifier),
|
|
89
|
+
model_name: normalizeValue(entry?.model_name),
|
|
90
|
+
source: normalizeValue(entry?.source),
|
|
91
|
+
registry: normalizeValue(entry?.registry),
|
|
92
|
+
version: normalizeValue(entry?.version),
|
|
93
|
+
license: normalizeValue(entry?.license),
|
|
94
|
+
digest: normalizeValue(entry?.digest),
|
|
95
|
+
violation_code: code,
|
|
96
|
+
rule_path: rulePath,
|
|
97
|
+
rule_id: deterministicRuleId(code, rulePath),
|
|
98
|
+
severity,
|
|
99
|
+
message: normalizeValue(violation.message, 'Policy violation detected.'),
|
|
100
|
+
expected: normalizeValue(violation.expected, ''),
|
|
101
|
+
actual: normalizeValue(violation.actual, ''),
|
|
102
|
+
recommendation: getRecommendationForCode(code),
|
|
103
|
+
exception_model: normalizeValue(entry?.exception?.model, ''),
|
|
104
|
+
exception_reason: normalizeValue(entry?.exception?.reason, ''),
|
|
105
|
+
exception_approver: normalizeValue(entry?.exception?.approver, ''),
|
|
106
|
+
exception_expires_at: normalizeValue(entry?.exception?.expires_at, '')
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractHardwareSummary(hardware = {}) {
|
|
111
|
+
return {
|
|
112
|
+
cpu: normalizeValue(hardware?.cpu?.brand || hardware?.cpu?.model),
|
|
113
|
+
cpu_cores: hardware?.cpu?.cores ?? 'unknown',
|
|
114
|
+
memory_gb: hardware?.memory?.total ?? 'unknown',
|
|
115
|
+
gpu: normalizeValue(hardware?.gpu?.model),
|
|
116
|
+
gpu_vram_gb: hardware?.gpu?.vram ?? 'unknown',
|
|
117
|
+
best_backend: normalizeValue(hardware?.summary?.bestBackend),
|
|
118
|
+
os_platform: normalizeValue(hardware?.os?.platform),
|
|
119
|
+
os_release: normalizeValue(hardware?.os?.release)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildComplianceReport({
|
|
124
|
+
commandName,
|
|
125
|
+
policyPath,
|
|
126
|
+
policy,
|
|
127
|
+
evaluation,
|
|
128
|
+
enforcement,
|
|
129
|
+
runtimeContext,
|
|
130
|
+
options,
|
|
131
|
+
hardware,
|
|
132
|
+
generatedAt
|
|
133
|
+
}) {
|
|
134
|
+
const timestamp = generatedAt || new Date().toISOString();
|
|
135
|
+
const findings = Array.isArray(evaluation?.findings)
|
|
136
|
+
? evaluation.findings.map((entry) => toFindingRecord(entry, timestamp))
|
|
137
|
+
: [];
|
|
138
|
+
|
|
139
|
+
const activeCount = findings.filter((entry) => entry.status === 'active').length;
|
|
140
|
+
const suppressedCount = findings.filter((entry) => entry.status === 'suppressed').length;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
schema_version: '1.0',
|
|
144
|
+
generated_at: timestamp,
|
|
145
|
+
command: normalizeValue(commandName),
|
|
146
|
+
policy: {
|
|
147
|
+
path: normalizeValue(policyPath),
|
|
148
|
+
org: normalizeValue(policy?.org),
|
|
149
|
+
mode: normalizeValue(policy?.mode),
|
|
150
|
+
on_violation: normalizeValue(policy?.enforcement?.on_violation, 'error'),
|
|
151
|
+
allow_exceptions: policy?.enforcement?.allow_exceptions === true,
|
|
152
|
+
reporting_formats: Array.isArray(policy?.reporting?.formats)
|
|
153
|
+
? policy.reporting.formats
|
|
154
|
+
: []
|
|
155
|
+
},
|
|
156
|
+
enforcement: {
|
|
157
|
+
should_block: Boolean(enforcement?.shouldBlock),
|
|
158
|
+
exit_code: enforcement?.exitCode ?? 0,
|
|
159
|
+
has_failures: Boolean(enforcement?.hasFailures),
|
|
160
|
+
mode: normalizeValue(enforcement?.mode, normalizeValue(policy?.mode)),
|
|
161
|
+
on_violation: normalizeValue(enforcement?.onViolation, 'error')
|
|
162
|
+
},
|
|
163
|
+
runtime: {
|
|
164
|
+
backend: normalizeValue(runtimeContext?.backend),
|
|
165
|
+
runtime_backend: normalizeValue(runtimeContext?.runtimeBackend),
|
|
166
|
+
ram_gb: runtimeContext?.ramGB ?? 'unknown'
|
|
167
|
+
},
|
|
168
|
+
hardware: extractHardwareSummary(hardware),
|
|
169
|
+
options: options || {},
|
|
170
|
+
summary: {
|
|
171
|
+
total_checked: evaluation?.totalChecked ?? 0,
|
|
172
|
+
pass_count: evaluation?.passCount ?? 0,
|
|
173
|
+
fail_count: evaluation?.failCount ?? 0,
|
|
174
|
+
active_violations: activeCount,
|
|
175
|
+
suppressed_violations: suppressedCount,
|
|
176
|
+
exceptions_applied: evaluation?.exceptionsAppliedCount ?? 0,
|
|
177
|
+
top_violations: Array.isArray(evaluation?.topViolations) ? evaluation.topViolations : []
|
|
178
|
+
},
|
|
179
|
+
findings
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function reportToJson(report) {
|
|
184
|
+
return JSON.stringify(report, null, 2);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function reportToCsv(report) {
|
|
188
|
+
const headers = [
|
|
189
|
+
'generated_at',
|
|
190
|
+
'command',
|
|
191
|
+
'policy_path',
|
|
192
|
+
'policy_mode',
|
|
193
|
+
'on_violation',
|
|
194
|
+
'total_checked',
|
|
195
|
+
'pass_count',
|
|
196
|
+
'fail_count',
|
|
197
|
+
'status',
|
|
198
|
+
'model_identifier',
|
|
199
|
+
'model_name',
|
|
200
|
+
'source',
|
|
201
|
+
'registry',
|
|
202
|
+
'version',
|
|
203
|
+
'license',
|
|
204
|
+
'digest',
|
|
205
|
+
'violation_code',
|
|
206
|
+
'rule_path',
|
|
207
|
+
'rule_id',
|
|
208
|
+
'severity',
|
|
209
|
+
'message',
|
|
210
|
+
'expected',
|
|
211
|
+
'actual',
|
|
212
|
+
'recommendation',
|
|
213
|
+
'exception_model',
|
|
214
|
+
'exception_reason',
|
|
215
|
+
'exception_approver',
|
|
216
|
+
'exception_expires_at'
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
const rows = [headers.join(',')];
|
|
220
|
+
|
|
221
|
+
if (!Array.isArray(report.findings) || report.findings.length === 0) {
|
|
222
|
+
rows.push(
|
|
223
|
+
[
|
|
224
|
+
report.generated_at,
|
|
225
|
+
report.command,
|
|
226
|
+
report.policy?.path,
|
|
227
|
+
report.policy?.mode,
|
|
228
|
+
report.policy?.on_violation,
|
|
229
|
+
report.summary?.total_checked,
|
|
230
|
+
report.summary?.pass_count,
|
|
231
|
+
report.summary?.fail_count,
|
|
232
|
+
'compliant',
|
|
233
|
+
'',
|
|
234
|
+
'',
|
|
235
|
+
'',
|
|
236
|
+
'',
|
|
237
|
+
'',
|
|
238
|
+
'',
|
|
239
|
+
'',
|
|
240
|
+
'',
|
|
241
|
+
'',
|
|
242
|
+
'',
|
|
243
|
+
'',
|
|
244
|
+
'No policy violations detected.',
|
|
245
|
+
'',
|
|
246
|
+
'',
|
|
247
|
+
'',
|
|
248
|
+
'',
|
|
249
|
+
'',
|
|
250
|
+
'',
|
|
251
|
+
''
|
|
252
|
+
]
|
|
253
|
+
.map(csvValue)
|
|
254
|
+
.join(',')
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return rows.join('\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
report.findings.forEach((finding) => {
|
|
261
|
+
rows.push(
|
|
262
|
+
[
|
|
263
|
+
finding.generated_at,
|
|
264
|
+
report.command,
|
|
265
|
+
report.policy?.path,
|
|
266
|
+
report.policy?.mode,
|
|
267
|
+
report.policy?.on_violation,
|
|
268
|
+
report.summary?.total_checked,
|
|
269
|
+
report.summary?.pass_count,
|
|
270
|
+
report.summary?.fail_count,
|
|
271
|
+
finding.status,
|
|
272
|
+
finding.model_identifier,
|
|
273
|
+
finding.model_name,
|
|
274
|
+
finding.source,
|
|
275
|
+
finding.registry,
|
|
276
|
+
finding.version,
|
|
277
|
+
finding.license,
|
|
278
|
+
finding.digest,
|
|
279
|
+
finding.violation_code,
|
|
280
|
+
finding.rule_path,
|
|
281
|
+
finding.rule_id,
|
|
282
|
+
finding.severity,
|
|
283
|
+
finding.message,
|
|
284
|
+
finding.expected,
|
|
285
|
+
finding.actual,
|
|
286
|
+
finding.recommendation,
|
|
287
|
+
finding.exception_model,
|
|
288
|
+
finding.exception_reason,
|
|
289
|
+
finding.exception_approver,
|
|
290
|
+
finding.exception_expires_at
|
|
291
|
+
]
|
|
292
|
+
.map(csvValue)
|
|
293
|
+
.join(',')
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return rows.join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function reportToSarif(report) {
|
|
301
|
+
const findings = Array.isArray(report.findings) ? report.findings : [];
|
|
302
|
+
const rulesMap = new Map();
|
|
303
|
+
|
|
304
|
+
findings.forEach((finding) => {
|
|
305
|
+
if (!rulesMap.has(finding.rule_id)) {
|
|
306
|
+
rulesMap.set(finding.rule_id, {
|
|
307
|
+
id: finding.rule_id,
|
|
308
|
+
name: finding.violation_code,
|
|
309
|
+
shortDescription: {
|
|
310
|
+
text: finding.violation_code
|
|
311
|
+
},
|
|
312
|
+
fullDescription: {
|
|
313
|
+
text: finding.message
|
|
314
|
+
},
|
|
315
|
+
help: {
|
|
316
|
+
text: finding.recommendation
|
|
317
|
+
},
|
|
318
|
+
properties: {
|
|
319
|
+
severity: finding.severity,
|
|
320
|
+
rulePath: finding.rule_path,
|
|
321
|
+
code: finding.violation_code
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const results = findings.map((finding) => {
|
|
328
|
+
const level = finding.status === 'suppressed' ? 'note' : sarifLevelFromSeverity(finding.severity);
|
|
329
|
+
const messagePrefix = finding.status === 'suppressed' ? '[suppressed by exception] ' : '';
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
ruleId: finding.rule_id,
|
|
333
|
+
level,
|
|
334
|
+
message: {
|
|
335
|
+
text: `${messagePrefix}${finding.message}`
|
|
336
|
+
},
|
|
337
|
+
locations: [
|
|
338
|
+
{
|
|
339
|
+
physicalLocation: {
|
|
340
|
+
artifactLocation: {
|
|
341
|
+
uri: finding.model_identifier
|
|
342
|
+
},
|
|
343
|
+
region: {
|
|
344
|
+
startLine: 1
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
],
|
|
349
|
+
properties: {
|
|
350
|
+
modelName: finding.model_name,
|
|
351
|
+
modelSource: finding.source,
|
|
352
|
+
policyPath: finding.rule_path,
|
|
353
|
+
expected: finding.expected,
|
|
354
|
+
actual: finding.actual,
|
|
355
|
+
recommendation: finding.recommendation,
|
|
356
|
+
status: finding.status,
|
|
357
|
+
exceptionReason: finding.exception_reason
|
|
358
|
+
},
|
|
359
|
+
partialFingerprints: {
|
|
360
|
+
ruleFingerprint: finding.rule_id,
|
|
361
|
+
modelFingerprint: `${finding.model_identifier}:${finding.violation_code}`
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const sarif = {
|
|
367
|
+
version: '2.1.0',
|
|
368
|
+
$schema:
|
|
369
|
+
'https://json.schemastore.org/sarif-2.1.0.json',
|
|
370
|
+
runs: [
|
|
371
|
+
{
|
|
372
|
+
tool: {
|
|
373
|
+
driver: {
|
|
374
|
+
name: 'llm-checker-policy',
|
|
375
|
+
version: '1.0.0',
|
|
376
|
+
informationUri: 'https://github.com/Pavelevich/llm-checker',
|
|
377
|
+
rules: Array.from(rulesMap.values())
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
invocations: [
|
|
381
|
+
{
|
|
382
|
+
executionSuccessful: true,
|
|
383
|
+
commandLine: `llm-checker ${report.command}`,
|
|
384
|
+
startTimeUtc: report.generated_at
|
|
385
|
+
}
|
|
386
|
+
],
|
|
387
|
+
results,
|
|
388
|
+
properties: {
|
|
389
|
+
policyPath: report.policy?.path,
|
|
390
|
+
policyMode: report.policy?.mode,
|
|
391
|
+
totalChecked: report.summary?.total_checked,
|
|
392
|
+
passCount: report.summary?.pass_count,
|
|
393
|
+
failCount: report.summary?.fail_count,
|
|
394
|
+
activeViolations: report.summary?.active_violations,
|
|
395
|
+
suppressedViolations: report.summary?.suppressed_violations
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return JSON.stringify(sarif, null, 2);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function serializeComplianceReport(report, format = 'json') {
|
|
405
|
+
const normalized = normalizeValue(format, 'json').toLowerCase();
|
|
406
|
+
|
|
407
|
+
if (normalized === 'json') return reportToJson(report);
|
|
408
|
+
if (normalized === 'csv') return reportToCsv(report);
|
|
409
|
+
if (normalized === 'sarif') return reportToSarif(report);
|
|
410
|
+
|
|
411
|
+
throw new Error(`Unsupported report format: ${format}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = {
|
|
415
|
+
buildComplianceReport,
|
|
416
|
+
serializeComplianceReport,
|
|
417
|
+
deterministicRuleId,
|
|
418
|
+
getSeverityForCode,
|
|
419
|
+
getRecommendationForCode
|
|
420
|
+
};
|