renma 0.6.1 → 0.8.0
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/CHANGELOG.md +36 -1
- package/README.md +19 -2
- package/dist/commands/ci-report.d.ts.map +1 -1
- package/dist/commands/ci-report.js +2 -1
- package/dist/commands/ci-report.js.map +1 -1
- package/dist/commands/diff.d.ts +1 -0
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +40 -5
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/readiness.js +2 -1
- package/dist/commands/readiness.js.map +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +2 -1
- package/dist/report.js.map +1 -1
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +6 -17
- package/dist/rules.js.map +1 -1
- package/dist/security-diagnostics.d.ts.map +1 -1
- package/dist/security-diagnostics.js +135 -26
- package/dist/security-diagnostics.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -18,6 +18,7 @@ const RULES = {
|
|
|
18
18
|
],
|
|
19
19
|
llmHint: "Add small frontmatter policy fields that describe whether network access, external uploads, and secret material are allowed for this artifact.",
|
|
20
20
|
confidence: "medium",
|
|
21
|
+
riskClass: "advisory",
|
|
21
22
|
},
|
|
22
23
|
policyContradiction: {
|
|
23
24
|
id: DIAGNOSTIC_IDS.SEC_POLICY_CONTRADICTION,
|
|
@@ -35,6 +36,7 @@ const RULES = {
|
|
|
35
36
|
],
|
|
36
37
|
llmHint: "Resolve the policy by choosing the stricter allowed behavior or by separating instructions into different assets with explicit metadata.",
|
|
37
38
|
confidence: "high",
|
|
39
|
+
riskClass: "violation",
|
|
38
40
|
},
|
|
39
41
|
bodyPolicyContradiction: {
|
|
40
42
|
id: DIAGNOSTIC_IDS.SEC_BODY_POLICY_CONTRADICTION,
|
|
@@ -52,6 +54,7 @@ const RULES = {
|
|
|
52
54
|
],
|
|
53
55
|
llmHint: "Resolve body and metadata conflicts by choosing the stricter behavior or separating conflicting instructions into different assets.",
|
|
54
56
|
confidence: "high",
|
|
57
|
+
riskClass: "violation",
|
|
55
58
|
},
|
|
56
59
|
policyProfileNotFound: {
|
|
57
60
|
id: DIAGNOSTIC_IDS.SEC_POLICY_PROFILE_NOT_FOUND,
|
|
@@ -69,6 +72,7 @@ const RULES = {
|
|
|
69
72
|
],
|
|
70
73
|
llmHint: "Use a configured security_profile value, or add the missing profile under security.profiles with explicit policy fields.",
|
|
71
74
|
confidence: "high",
|
|
75
|
+
riskClass: "violation",
|
|
72
76
|
},
|
|
73
77
|
policyProfileCycle: {
|
|
74
78
|
id: DIAGNOSTIC_IDS.SEC_POLICY_PROFILE_CYCLE,
|
|
@@ -86,6 +90,7 @@ const RULES = {
|
|
|
86
90
|
],
|
|
87
91
|
llmHint: "Remove or rewrite the cyclic profile reference so the selected security profile has a deterministic parent chain.",
|
|
88
92
|
confidence: "high",
|
|
93
|
+
riskClass: "violation",
|
|
89
94
|
},
|
|
90
95
|
policyOverrideContradiction: {
|
|
91
96
|
id: DIAGNOSTIC_IDS.SEC_POLICY_OVERRIDE_CONTRADICTION,
|
|
@@ -103,6 +108,7 @@ const RULES = {
|
|
|
103
108
|
],
|
|
104
109
|
llmHint: "Treat explicit false policy fields in the artifact as authoritative and adjust the referenced profile or repo-level security config.",
|
|
105
110
|
confidence: "high",
|
|
111
|
+
riskClass: "violation",
|
|
106
112
|
},
|
|
107
113
|
forbiddenInputInstruction: {
|
|
108
114
|
id: DIAGNOSTIC_IDS.SEC_FORBIDDEN_INPUT_INSTRUCTION,
|
|
@@ -120,6 +126,7 @@ const RULES = {
|
|
|
120
126
|
],
|
|
121
127
|
llmHint: "Rewrite the instruction so it avoids profile-forbidden inputs such as secrets, credentials, private keys, or customer data.",
|
|
122
128
|
confidence: "high",
|
|
129
|
+
riskClass: "violation",
|
|
123
130
|
},
|
|
124
131
|
instructionViolatesPolicy: {
|
|
125
132
|
id: DIAGNOSTIC_IDS.SEC_INSTRUCTION_VIOLATES_POLICY,
|
|
@@ -137,6 +144,7 @@ const RULES = {
|
|
|
137
144
|
],
|
|
138
145
|
llmHint: "Find the instruction that asks for denied behavior and rewrite it to stay within the artifact's declared security policy.",
|
|
139
146
|
confidence: "high",
|
|
147
|
+
riskClass: "violation",
|
|
140
148
|
},
|
|
141
149
|
missingHumanApprovalGuard: {
|
|
142
150
|
id: DIAGNOSTIC_IDS.SEC_MISSING_HUMAN_APPROVAL_GUARD,
|
|
@@ -154,6 +162,7 @@ const RULES = {
|
|
|
154
162
|
],
|
|
155
163
|
llmHint: "Insert an explicit human approval requirement next to upload, POST, cloud sync, or external sharing instructions.",
|
|
156
164
|
confidence: "medium",
|
|
165
|
+
riskClass: "violation",
|
|
157
166
|
},
|
|
158
167
|
sensitiveFileReference: {
|
|
159
168
|
id: DIAGNOSTIC_IDS.SEC_SENSITIVE_FILE_REFERENCE,
|
|
@@ -171,6 +180,7 @@ const RULES = {
|
|
|
171
180
|
],
|
|
172
181
|
llmHint: "Inspect this reference and either replace it with a safe placeholder or add explicit no-disclosure handling instructions.",
|
|
173
182
|
confidence: "high",
|
|
183
|
+
riskClass: "violation",
|
|
174
184
|
},
|
|
175
185
|
secretMaterialInstruction: {
|
|
176
186
|
id: DIAGNOSTIC_IDS.SEC_SECRET_MATERIAL_INSTRUCTION,
|
|
@@ -188,6 +198,7 @@ const RULES = {
|
|
|
188
198
|
],
|
|
189
199
|
llmHint: "Rewrite this instruction so secret-bearing files are never copied into prompts, logs, uploads, or diagnostics.",
|
|
190
200
|
confidence: "high",
|
|
201
|
+
riskClass: "violation",
|
|
191
202
|
},
|
|
192
203
|
externalUploadInstruction: {
|
|
193
204
|
id: DIAGNOSTIC_IDS.SEC_EXTERNAL_UPLOAD_INSTRUCTION,
|
|
@@ -205,6 +216,7 @@ const RULES = {
|
|
|
205
216
|
],
|
|
206
217
|
llmHint: "Add a human approval gate and approved destination metadata, or replace the upload with a local-only workflow.",
|
|
207
218
|
confidence: "high",
|
|
219
|
+
riskClass: "suspicious",
|
|
208
220
|
},
|
|
209
221
|
unapprovedNetworkDestination: {
|
|
210
222
|
id: DIAGNOSTIC_IDS.SEC_UNAPPROVED_NETWORK_DESTINATION,
|
|
@@ -222,6 +234,7 @@ const RULES = {
|
|
|
222
234
|
],
|
|
223
235
|
llmHint: "Compare the referenced URL or host to approved_network_destinations and either approve it explicitly or remove the instruction.",
|
|
224
236
|
confidence: "high",
|
|
237
|
+
riskClass: "violation",
|
|
225
238
|
},
|
|
226
239
|
unapprovedUploadDestination: {
|
|
227
240
|
id: DIAGNOSTIC_IDS.SEC_UNAPPROVED_UPLOAD_DESTINATION,
|
|
@@ -239,6 +252,7 @@ const RULES = {
|
|
|
239
252
|
],
|
|
240
253
|
llmHint: "Compare the referenced upload URL or host to security.approvedUploadDomains and either approve it explicitly or remove the instruction.",
|
|
241
254
|
confidence: "high",
|
|
255
|
+
riskClass: "violation",
|
|
242
256
|
},
|
|
243
257
|
bulkDataSharingInstruction: {
|
|
244
258
|
id: DIAGNOSTIC_IDS.SEC_BULK_DATA_SHARING_INSTRUCTION,
|
|
@@ -256,6 +270,7 @@ const RULES = {
|
|
|
256
270
|
],
|
|
257
271
|
llmHint: "Replace broad sharing language with scoped file paths, limited snippets, and redaction requirements.",
|
|
258
272
|
confidence: "medium",
|
|
273
|
+
riskClass: "suspicious",
|
|
259
274
|
},
|
|
260
275
|
cloudUploadInstruction: {
|
|
261
276
|
id: DIAGNOSTIC_IDS.SEC_CLOUD_UPLOAD_INSTRUCTION,
|
|
@@ -273,6 +288,7 @@ const RULES = {
|
|
|
273
288
|
],
|
|
274
289
|
llmHint: "Turn the cloud upload into a local-only output, or add policy metadata and a human approval guard.",
|
|
275
290
|
confidence: "medium",
|
|
291
|
+
riskClass: "suspicious",
|
|
276
292
|
},
|
|
277
293
|
overbroadContextInstruction: {
|
|
278
294
|
id: DIAGNOSTIC_IDS.SEC_OVERBROAD_CONTEXT_INSTRUCTION,
|
|
@@ -290,6 +306,7 @@ const RULES = {
|
|
|
290
306
|
],
|
|
291
307
|
llmHint: "Replace broad context collection with bounded paths, task-relevant snippets, and explicit exclusions for secrets.",
|
|
292
308
|
confidence: "medium",
|
|
309
|
+
riskClass: "suspicious",
|
|
293
310
|
},
|
|
294
311
|
noRedactionInstruction: {
|
|
295
312
|
id: DIAGNOSTIC_IDS.SEC_NO_REDACTION_INSTRUCTION,
|
|
@@ -307,6 +324,7 @@ const RULES = {
|
|
|
307
324
|
],
|
|
308
325
|
llmHint: "Replace no-redaction wording with explicit redaction requirements for secrets and sensitive data.",
|
|
309
326
|
confidence: "high",
|
|
327
|
+
riskClass: "violation",
|
|
310
328
|
},
|
|
311
329
|
unpinnedRemoteScript: {
|
|
312
330
|
id: DIAGNOSTIC_IDS.SEC_UNPINNED_REMOTE_SCRIPT,
|
|
@@ -324,6 +342,7 @@ const RULES = {
|
|
|
324
342
|
],
|
|
325
343
|
llmHint: "Rewrite the install instruction to download a pinned artifact and verify it before execution.",
|
|
326
344
|
confidence: "high",
|
|
345
|
+
riskClass: "suspicious",
|
|
327
346
|
},
|
|
328
347
|
unpinnedDependencyInstall: {
|
|
329
348
|
id: DIAGNOSTIC_IDS.SEC_UNPINNED_DEPENDENCY_INSTALL,
|
|
@@ -341,6 +360,7 @@ const RULES = {
|
|
|
341
360
|
],
|
|
342
361
|
llmHint: "Pin packages, images, or formulas in setup instructions, or route through the repository's lockfile command.",
|
|
343
362
|
confidence: "medium",
|
|
363
|
+
riskClass: "suspicious",
|
|
344
364
|
},
|
|
345
365
|
privilegedCommandWithoutGuard: {
|
|
346
366
|
id: DIAGNOSTIC_IDS.SEC_PRIVILEGED_COMMAND_WITHOUT_GUARD,
|
|
@@ -358,6 +378,27 @@ const RULES = {
|
|
|
358
378
|
],
|
|
359
379
|
llmHint: "Add an explicit approval requirement before sudo, chmod/chown, docker privileged operations, or system writes.",
|
|
360
380
|
confidence: "medium",
|
|
381
|
+
riskClass: "suspicious",
|
|
382
|
+
},
|
|
383
|
+
destructiveCommand: {
|
|
384
|
+
id: DIAGNOSTIC_IDS.SEC_DESTRUCTIVE_COMMAND,
|
|
385
|
+
category: "safety",
|
|
386
|
+
title: "Destructive command lacks a review guard",
|
|
387
|
+
whyItMatters: "Destructive commands in agent-facing guidance can erase files, reset Git state, remove containers, or delete infrastructure when copied or followed by an agent.",
|
|
388
|
+
remediation: "Remove the destructive command, replace it with a safer scoped command, or add explicit human approval, dry-run, backup, and rollback guidance.",
|
|
389
|
+
constraints: [
|
|
390
|
+
"Do not normalize destructive commands as routine setup.",
|
|
391
|
+
"Keep any required destructive action narrowly scoped.",
|
|
392
|
+
"Keep approval and recovery guidance close to the command.",
|
|
393
|
+
],
|
|
394
|
+
verificationSteps: [
|
|
395
|
+
"Run renma scan.",
|
|
396
|
+
"Confirm destructive commands are removed or guarded.",
|
|
397
|
+
"Review any remaining command for scope, backup, and rollback guidance.",
|
|
398
|
+
],
|
|
399
|
+
llmHint: "Replace forced deletion, hard reset, clean, prune, or delete commands with safer alternatives, or add explicit approval plus verification and rollback steps.",
|
|
400
|
+
confidence: "high",
|
|
401
|
+
riskClass: "violation",
|
|
361
402
|
},
|
|
362
403
|
dangerousToolInstruction: {
|
|
363
404
|
id: DIAGNOSTIC_IDS.SEC_DANGEROUS_TOOL_INSTRUCTION,
|
|
@@ -375,6 +416,7 @@ const RULES = {
|
|
|
375
416
|
],
|
|
376
417
|
llmHint: "Check security.disallowedCommands and remove instructions that invoke those commands or services.",
|
|
377
418
|
confidence: "high",
|
|
419
|
+
riskClass: "violation",
|
|
378
420
|
},
|
|
379
421
|
credentialInCommandArg: {
|
|
380
422
|
id: DIAGNOSTIC_IDS.SEC_CREDENTIAL_IN_COMMAND_ARG,
|
|
@@ -392,6 +434,7 @@ const RULES = {
|
|
|
392
434
|
],
|
|
393
435
|
llmHint: "Replace literal credential command arguments with safe placeholders and approved secret handling guidance.",
|
|
394
436
|
confidence: "high",
|
|
437
|
+
riskClass: "violation",
|
|
395
438
|
},
|
|
396
439
|
predictableTempPath: {
|
|
397
440
|
id: DIAGNOSTIC_IDS.SEC_PREDICTABLE_TEMP_PATH,
|
|
@@ -409,6 +452,7 @@ const RULES = {
|
|
|
409
452
|
],
|
|
410
453
|
llmHint: "Replace predictable /tmp paths for profiles, credentials, certs, logs, or tokens with secure temporary directory handling.",
|
|
411
454
|
confidence: "medium",
|
|
455
|
+
riskClass: "suspicious",
|
|
412
456
|
},
|
|
413
457
|
};
|
|
414
458
|
const BOOLEAN_POLICY_FIELDS = new Map([
|
|
@@ -465,11 +509,15 @@ const BULK_DATA_RE = /\b(entire|whole|all|full|complete|raw)\b.*\b(repo|reposito
|
|
|
465
509
|
const UNDISCLOSED_DATA_RE = /\b(include|attach|paste|upload|send|share|dump|export|print|collect|provide)\b.*\b(all|entire|full|complete|raw)\b.*\b(environment variables|env vars|env|process\.env|secrets?|credentials?|tokens?)\b/i;
|
|
466
510
|
const OVERBROAD_CONTEXT_RE = /\b(load|read|include|attach|paste|ingest|collect|provide|send)\b.*\b(entire|whole|all|full|complete|raw)\b.*\b(repo|repository|workspace|codebase|context|logs?|files?)\b/i;
|
|
467
511
|
const NO_REDACTION_RE = /\b(do not|don't|without|no|never)\b.{0,30}\b(redact|redaction|sanitize|mask|obfuscate)\b|\b(redact|sanitize|mask|obfuscate)\b.{0,30}\b(disabled|false|off)\b/i;
|
|
468
|
-
const APPROVAL_RE = /\b(human|user|owner|maintainer|reviewer|security)\b.{0,
|
|
512
|
+
const APPROVAL_RE = /\b(ask|prompt|require|obtain|wait for)\b.{0,50}\b(human|user|owner|maintainer|reviewer|security)?\s*(approval|confirmation|consent|authorization|review)\b|\b(human|user|owner|maintainer|reviewer|security)\b.{0,50}\b(approve|approval|confirm|confirmation|review|consent|authorize|authorization)\b|\bonly\b.{0,20}\b(after|with)\b.{0,40}\b(explicit\s+)?(human|user|owner|maintainer|reviewer|security)?\s*(approval|confirmation|review|authorization)\b|\bdo\s+not\s+run\s+automatically\b.{0,60}\b(human|user|maintainer|review|approval|confirmation)\b/i;
|
|
513
|
+
const WEAK_OR_NEGATED_APPROVAL_RE = /\b(no approval|approval is not|approval isn't|approved by default|approval by default|without approval|automatically approved|safe|run carefully|make sure it works)\b/i;
|
|
514
|
+
const RECOVERY_GUARD_RE = /\b(create|make|take|keep|verify|confirm|document|check|use|run)\b.{0,40}\b(backup|rollback|roll back|restore|dry[- ]run|revert)\b|\b(backup|rollback|roll back|restore|dry[- ]run|revert)\b.{0,40}\b(first|before|steps?|plan|guidance|confirm|verify|check)\b/i;
|
|
469
515
|
const SECRET_WORD_RE = /\b(secret|secrets|credential|credentials|token|password|passwd|api key|apikey|private key|ssh key|signing key|certificate|cert|auth)\b/i;
|
|
470
516
|
const SECRET_ACTION_RE = /\b(copy|print|cat|echo|paste|upload|send|share|attach|include|dump|export|log|summari[sz]e|read)\b/i;
|
|
471
517
|
const SAFE_NEGATION_RE = /\b(not|never|avoid|exclude|without|redact|mock|fake|sample|placeholder|dummy)\b.{0,40}\b(secret|secrets|credential|credentials|token|password|private key)\b|\b(secret|secrets|credential|credentials|token|password|private key)\b.{0,40}\b(not|never|avoid|exclude|redact|mock|fake|sample|placeholder|dummy)\b/i;
|
|
472
|
-
const
|
|
518
|
+
const DEFENSIVE_ACTION_RE = /\b(do\s+not|don't|never|avoid|exclude|skip|omit|forbid|forbidden|disallow|block)\b.{0,80}\b(upload|send|post|put|share|attach|submit|sync|push|publish|copy|paste|include|print|cat|echo|log|dump|curl|wget|pipe|bash|sh|sudo|chmod|chown|rm\s+-|git\s+reset|git\s+clean|delete|install|add)\b/i;
|
|
519
|
+
const GUARDED_ACTION_RE = /\b(only|unless|after|with|before)\b.{0,80}\b(approval|approved|confirmation|confirm|human review|maintainer review|redact|redacted|redaction|dry[- ]run|backup|rollback)\b|\b(redact|redacted|redaction|approval|approved|confirmation|confirm|human review|maintainer review|dry[- ]run|backup|rollback)\b.{0,80}\b(before|after|upload|send|post|put|share|sudo|rm\s+-|git\s+reset|git\s+clean|delete|install|add)\b/i;
|
|
520
|
+
const REMOTE_SCRIPT_RE = /\b(curl|wget)\b[^\n]*?(https?:\/\/[^\s|`'")]+)[^\n]*\|\s*(sh|bash|zsh)\b/i;
|
|
473
521
|
const PRIVILEGED_COMMAND_RE = /\b(sudo|chmod\s+(777|666|\+w|a\+w)|chown\b|docker\s+run\b[^\n]*(--privileged|-v\s+\/|--pid=host)|mount\b|launchctl\b|systemctl\b)\b/i;
|
|
474
522
|
const DESTRUCTIVE_COMMAND_RE = /\b(rm\s+-[^\n]*[rf][^\n]*|git\s+reset\s+--hard|git\s+clean\s+-[^\n]*[xdf][^\n]*|docker\s+(?:rm|rmi|system\s+prune|volume\s+rm)\b|kubectl\s+delete\b|drop\s+database|truncate\s+table)\b/i;
|
|
475
523
|
const CREDENTIAL_ARG_RE = /--?(token|password|passwd|secret|credential|api[-_]?key|key|cert|certificate|signing[-_]?key|auth)(=|\s+)(?!<|\$|\{|\[|REDACTED|redacted|xxx|XXX|placeholder|example)[^\s"'`]+/i;
|
|
@@ -503,7 +551,8 @@ function securityFindingsForArtifact(artifact, securityConfig) {
|
|
|
503
551
|
: -1;
|
|
504
552
|
const scanStart = frontmatterEnd > 0 ? frontmatterEnd + 1 : 0;
|
|
505
553
|
let inFence = false;
|
|
506
|
-
let
|
|
554
|
+
let recentHumanApprovalLine = 0;
|
|
555
|
+
let recentRiskMitigationLine = 0;
|
|
507
556
|
if ((artifact.kind === "skill" || artifact.kind === "context") &&
|
|
508
557
|
effectiveAllowedDataClass(policy) === undefined &&
|
|
509
558
|
effectiveAllowedDataList(policy).length === 0) {
|
|
@@ -534,28 +583,36 @@ function securityFindingsForArtifact(artifact, securityConfig) {
|
|
|
534
583
|
if (isPolicyLine(line)) {
|
|
535
584
|
continue;
|
|
536
585
|
}
|
|
537
|
-
const
|
|
538
|
-
(
|
|
586
|
+
const hasHumanApprovalGuard = hasExplicitHumanApprovalGuard(line) ||
|
|
587
|
+
(recentHumanApprovalLine > 0 &&
|
|
588
|
+
lineNumber - recentHumanApprovalLine <= 2);
|
|
589
|
+
const hasCommandRiskGuard = hasHumanApprovalGuard ||
|
|
590
|
+
hasLocalRiskMitigationGuard(line) ||
|
|
591
|
+
(recentRiskMitigationLine > 0 &&
|
|
592
|
+
lineNumber - recentRiskMitigationLine <= 2);
|
|
539
593
|
const commandLine = !shellComment &&
|
|
540
594
|
(inFence ||
|
|
541
595
|
isCommandLike(line) ||
|
|
542
596
|
CREDENTIAL_ARG_ANY_RE.test(line) ||
|
|
543
597
|
CREDENTIAL_HEADER_RE.test(line));
|
|
544
|
-
detections.push(...policyDetections(line, lineNumber, policy,
|
|
598
|
+
detections.push(...policyDetections(line, lineNumber, policy, hasHumanApprovalGuard));
|
|
545
599
|
detections.push(...disallowedCommandDetections(line, lineNumber, policy));
|
|
546
600
|
if (!commandLine || referencesSensitiveFile(line)) {
|
|
547
601
|
detections.push(...sensitiveDataDetections(line, lineNumber, policy));
|
|
548
602
|
}
|
|
549
|
-
if (!commandLine || policy.declared.size > 0) {
|
|
603
|
+
if (!commandLine || policy.declared.size > 0 || isUploadInstruction(line)) {
|
|
550
604
|
detections.push(...networkAndUploadDetections(line, lineNumber, policy));
|
|
551
605
|
}
|
|
552
606
|
detections.push(...contextScopeDetections(line, lineNumber));
|
|
553
607
|
detections.push(...predictableTempDetections(line, lineNumber));
|
|
554
608
|
if (commandLine) {
|
|
555
|
-
detections.push(...commandDetections(line, lineNumber,
|
|
609
|
+
detections.push(...commandDetections(line, lineNumber, hasCommandRiskGuard));
|
|
556
610
|
}
|
|
557
|
-
if (
|
|
558
|
-
|
|
611
|
+
if (hasExplicitHumanApprovalGuard(line)) {
|
|
612
|
+
recentHumanApprovalLine = lineNumber;
|
|
613
|
+
}
|
|
614
|
+
if (hasLocalRiskMitigationGuard(line)) {
|
|
615
|
+
recentRiskMitigationLine = lineNumber;
|
|
559
616
|
}
|
|
560
617
|
}
|
|
561
618
|
detections.push(...policyContradictions(policy));
|
|
@@ -623,9 +680,13 @@ function bodyPolicyContradictionDetections(content, policy) {
|
|
|
623
680
|
}
|
|
624
681
|
return detections;
|
|
625
682
|
}
|
|
626
|
-
function policyDetections(line, lineNumber, policy,
|
|
683
|
+
function policyDetections(line, lineNumber, policy, hasHumanApprovalGuard) {
|
|
627
684
|
const detections = [];
|
|
628
|
-
|
|
685
|
+
const defensiveAction = isDefensiveActionInstruction(line);
|
|
686
|
+
const safeOrGuarded = isDefensiveOrGuardedActionInstruction(line);
|
|
687
|
+
if (policy.networkAllowed === false &&
|
|
688
|
+
NETWORK_ACTION_RE.test(line) &&
|
|
689
|
+
!safeOrGuarded) {
|
|
629
690
|
detections.push({
|
|
630
691
|
metadata: RULES.instructionViolatesPolicy,
|
|
631
692
|
severity: "high",
|
|
@@ -645,7 +706,9 @@ function policyDetections(line, lineNumber, policy, hasApprovalGuard) {
|
|
|
645
706
|
});
|
|
646
707
|
}
|
|
647
708
|
}
|
|
648
|
-
if (policy.externalUploadAllowed === false &&
|
|
709
|
+
if (policy.externalUploadAllowed === false &&
|
|
710
|
+
isUploadInstruction(line) &&
|
|
711
|
+
!safeOrGuarded) {
|
|
649
712
|
detections.push({
|
|
650
713
|
metadata: RULES.instructionViolatesPolicy,
|
|
651
714
|
severity: "high",
|
|
@@ -685,9 +748,10 @@ function policyDetections(line, lineNumber, policy, hasApprovalGuard) {
|
|
|
685
748
|
snippet: line,
|
|
686
749
|
});
|
|
687
750
|
}
|
|
688
|
-
const needsApproval =
|
|
689
|
-
|
|
690
|
-
!
|
|
751
|
+
const needsApproval = policy.humanApprovalRequired === true &&
|
|
752
|
+
requiresHumanApprovalGuard(line) &&
|
|
753
|
+
!hasHumanApprovalGuard &&
|
|
754
|
+
!defensiveAction;
|
|
691
755
|
if (needsApproval) {
|
|
692
756
|
detections.push({
|
|
693
757
|
metadata: RULES.missingHumanApprovalGuard,
|
|
@@ -755,6 +819,9 @@ function isSafeSensitiveHandlingInstruction(line) {
|
|
|
755
819
|
}
|
|
756
820
|
function networkAndUploadDetections(line, lineNumber, policy) {
|
|
757
821
|
const detections = [];
|
|
822
|
+
if (isDefensiveOrGuardedActionInstruction(line)) {
|
|
823
|
+
return detections;
|
|
824
|
+
}
|
|
758
825
|
if (EXTERNAL_UPLOAD_RE.test(line)) {
|
|
759
826
|
detections.push({
|
|
760
827
|
metadata: RULES.externalUploadInstruction,
|
|
@@ -801,22 +868,24 @@ function contextScopeDetections(line, lineNumber) {
|
|
|
801
868
|
}
|
|
802
869
|
return detections;
|
|
803
870
|
}
|
|
804
|
-
function commandDetections(line, lineNumber,
|
|
871
|
+
function commandDetections(line, lineNumber, hasCommandRiskGuard) {
|
|
805
872
|
const detections = [];
|
|
873
|
+
const defensiveAction = isDefensiveOrGuardedActionInstruction(line);
|
|
806
874
|
const remoteScript = line.match(REMOTE_SCRIPT_RE);
|
|
807
|
-
if (remoteScript && !hasPinnedRemoteScript(line)) {
|
|
875
|
+
if (remoteScript && !hasPinnedRemoteScript(line) && !defensiveAction) {
|
|
876
|
+
const fetchCommand = remoteScript[1] ?? "curl";
|
|
808
877
|
const remoteUrl = (remoteScript[2] ?? line).replace(/[.,;:]+$/, "");
|
|
809
|
-
const shell = remoteScript[
|
|
878
|
+
const shell = remoteScript[3] ?? "sh";
|
|
810
879
|
detections.push({
|
|
811
880
|
metadata: RULES.unpinnedRemoteScript,
|
|
812
881
|
severity: "high",
|
|
813
882
|
startLine: lineNumber,
|
|
814
|
-
snippet:
|
|
883
|
+
snippet: `${fetchCommand} ${remoteUrl} | ${shell}`,
|
|
815
884
|
dedupeKey: `${RULES.unpinnedRemoteScript.id}:${remoteUrl}`,
|
|
816
885
|
});
|
|
817
886
|
}
|
|
818
887
|
const unpinnedInstall = unpinnedDependencyInstall(line);
|
|
819
|
-
if (unpinnedInstall) {
|
|
888
|
+
if (unpinnedInstall && !defensiveAction) {
|
|
820
889
|
detections.push({
|
|
821
890
|
metadata: RULES.unpinnedDependencyInstall,
|
|
822
891
|
severity: "medium",
|
|
@@ -824,7 +893,9 @@ function commandDetections(line, lineNumber, hasApprovalGuard) {
|
|
|
824
893
|
snippet: line,
|
|
825
894
|
});
|
|
826
895
|
}
|
|
827
|
-
if (PRIVILEGED_COMMAND_RE.test(line) &&
|
|
896
|
+
if (PRIVILEGED_COMMAND_RE.test(line) &&
|
|
897
|
+
!hasCommandRiskGuard &&
|
|
898
|
+
!defensiveAction) {
|
|
828
899
|
detections.push({
|
|
829
900
|
metadata: RULES.privilegedCommandWithoutGuard,
|
|
830
901
|
severity: "medium",
|
|
@@ -832,6 +903,16 @@ function commandDetections(line, lineNumber, hasApprovalGuard) {
|
|
|
832
903
|
snippet: line,
|
|
833
904
|
});
|
|
834
905
|
}
|
|
906
|
+
if (DESTRUCTIVE_COMMAND_RE.test(line) &&
|
|
907
|
+
!hasCommandRiskGuard &&
|
|
908
|
+
!defensiveAction) {
|
|
909
|
+
detections.push({
|
|
910
|
+
metadata: RULES.destructiveCommand,
|
|
911
|
+
severity: "high",
|
|
912
|
+
startLine: lineNumber,
|
|
913
|
+
snippet: line,
|
|
914
|
+
});
|
|
915
|
+
}
|
|
835
916
|
if (CREDENTIAL_ARG_RE.test(line) || CREDENTIAL_HEADER_RE.test(line)) {
|
|
836
917
|
detections.push({
|
|
837
918
|
metadata: RULES.credentialInCommandArg,
|
|
@@ -1307,9 +1388,36 @@ function isPolicyLine(line) {
|
|
|
1307
1388
|
function isCommandLike(line) {
|
|
1308
1389
|
return /\b(npm|pnpm|yarn|pip3?|brew|docker|curl|wget|sudo|chmod|chown|git|gh|aws|gcloud|az|kubectl|echo|cat|cp|mv|rm|touch|mkdir)\b/i.test(line);
|
|
1309
1390
|
}
|
|
1310
|
-
function
|
|
1311
|
-
|
|
1312
|
-
|
|
1391
|
+
function hasExplicitHumanApprovalGuard(line) {
|
|
1392
|
+
if (WEAK_OR_NEGATED_APPROVAL_RE.test(line)) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
return APPROVAL_RE.test(line);
|
|
1396
|
+
}
|
|
1397
|
+
function hasLocalRiskMitigationGuard(line) {
|
|
1398
|
+
return RECOVERY_GUARD_RE.test(line);
|
|
1399
|
+
}
|
|
1400
|
+
function requiresHumanApprovalGuard(line) {
|
|
1401
|
+
return (EXTERNAL_UPLOAD_RE.test(line) ||
|
|
1402
|
+
CLOUD_UPLOAD_RE.test(line) ||
|
|
1403
|
+
referencesConcreteNetworkDestination(line) ||
|
|
1404
|
+
(SECRET_ACTION_RE.test(line) && SECRET_WORD_RE.test(line)) ||
|
|
1405
|
+
(referencesSensitiveFile(line) &&
|
|
1406
|
+
!isSafeSensitiveHandlingInstruction(line)) ||
|
|
1407
|
+
PRIVILEGED_COMMAND_RE.test(line) ||
|
|
1408
|
+
DESTRUCTIVE_COMMAND_RE.test(line));
|
|
1409
|
+
}
|
|
1410
|
+
function referencesConcreteNetworkDestination(line) {
|
|
1411
|
+
return (NETWORK_ACTION_RE.test(line) && extractNetworkDestinations(line).length > 0);
|
|
1412
|
+
}
|
|
1413
|
+
function isDefensiveOrGuardedActionInstruction(line) {
|
|
1414
|
+
return isDefensiveActionInstruction(line) || GUARDED_ACTION_RE.test(line);
|
|
1415
|
+
}
|
|
1416
|
+
function isDefensiveActionInstruction(line) {
|
|
1417
|
+
if (/\b(no approval is needed|approved by default|safe to run)\b/i.test(line)) {
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
return DEFENSIVE_ACTION_RE.test(line);
|
|
1313
1421
|
}
|
|
1314
1422
|
function hasPinnedRemoteScript(line) {
|
|
1315
1423
|
return /\b(sha256|sha512|checksum|gpg|cosign|sigstore|version|v\d+\.\d+\.\d+|@[a-f0-9]{7,40})\b/i.test(line);
|
|
@@ -1323,7 +1431,7 @@ function unpinnedDependencyInstall(line) {
|
|
|
1323
1431
|
if (pnpm && splitCommandArgs(pnpm[1] ?? "").some(isUnpinnedNpmPackage)) {
|
|
1324
1432
|
return true;
|
|
1325
1433
|
}
|
|
1326
|
-
const yarn = line.match(/\byarn\s+add\s+([^\n#]+)/i);
|
|
1434
|
+
const yarn = line.match(/\byarn\s+(?:global\s+)?add\s+([^\n#]+)/i);
|
|
1327
1435
|
if (yarn && splitCommandArgs(yarn[1] ?? "").some(isUnpinnedNpmPackage)) {
|
|
1328
1436
|
return true;
|
|
1329
1437
|
}
|
|
@@ -1414,6 +1522,7 @@ function findingFromDetection(artifact, detection) {
|
|
|
1414
1522
|
verificationSteps: detection.metadata.verificationSteps,
|
|
1415
1523
|
llmHint: detection.metadata.llmHint,
|
|
1416
1524
|
confidence: detection.metadata.confidence,
|
|
1525
|
+
riskClass: detection.metadata.riskClass,
|
|
1417
1526
|
};
|
|
1418
1527
|
}
|
|
1419
1528
|
function snippet(value) {
|