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.
@@ -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,40}\b(approve|approval|confirm|confirmation|review|consent|authorize|authorization)\b|\b(ask|require|obtain|wait for)\b.{0,40}\b(approval|confirmation|consent|authorization)\b/i;
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 REMOTE_SCRIPT_RE = /\b(curl|wget)\b[^\n]*?(https?:\/\/[^\s|`'")]+)[^\n]*(\|\s*(sh|bash|zsh)|\b(sh|bash|zsh)\b)/i;
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 recentApprovalLine = 0;
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 hasApprovalGuard = hasNearbyApprovalLanguage(line) ||
538
- (recentApprovalLine > 0 && lineNumber - recentApprovalLine <= 2);
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, hasApprovalGuard));
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, hasApprovalGuard));
609
+ detections.push(...commandDetections(line, lineNumber, hasCommandRiskGuard));
556
610
  }
557
- if (hasNearbyApprovalLanguage(line)) {
558
- recentApprovalLine = lineNumber;
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, hasApprovalGuard) {
683
+ function policyDetections(line, lineNumber, policy, hasHumanApprovalGuard) {
627
684
  const detections = [];
628
- if (policy.networkAllowed === false && NETWORK_ACTION_RE.test(line)) {
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 && isUploadInstruction(line)) {
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 = (EXTERNAL_UPLOAD_RE.test(line) || CLOUD_UPLOAD_RE.test(line)) &&
689
- policy.humanApprovalRequired === true &&
690
- !hasApprovalGuard;
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, hasApprovalGuard) {
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[4] ?? remoteScript[5] ?? "sh";
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: `curl ${remoteUrl} | ${shell}`,
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) && !hasApprovalGuard) {
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 hasNearbyApprovalLanguage(line) {
1311
- return (APPROVAL_RE.test(line) ||
1312
- /\b(approved|approval|confirm|confirmed|ask|asking|asked|review|reviewed|authorize|authorized|permission|consent|guardrail|guard)\b/i.test(line));
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) {