@node9/policy-engine 2.13.1 → 2.14.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/dist/index.d.mts CHANGED
@@ -207,6 +207,25 @@ declare function detectDangerousShellExec(command: string): 'block' | 'review' |
207
207
  /** @deprecated Use detectDangerousShellExec — kept for backwards compatibility */
208
208
  declare const detectDangerousEval: typeof detectDangerousShellExec;
209
209
  declare const FS_READ_TOOLS: Set<string>;
210
+ /** How a copy verb's arguments are read. */
211
+ interface CopyShape {
212
+ /**
213
+ * allButLast cp SRC... DEST (GNU -t moves DEST into a flag: every slot is a source)
214
+ * first ln TARGET LINK
215
+ * all gzip -c FILE
216
+ * archive tar/zip/ar/7z: the inputs after the archive slot, in a WRITING mode
217
+ * flagOperand the source is a flag's operand: az ... upload -f SRC / --file SRC
218
+ */
219
+ source: 'allButLast' | 'first' | 'all' | 'archive' | 'flagOperand';
220
+ archive?: 'tar' | 'zip' | 'ar' | '7z';
221
+ /** flagOperand: the flags (short letter or long name) whose operand is the source. */
222
+ sourceFlags?: string[];
223
+ /** GNU `-t DIR` / `--target-directory`: the destination is in a flag. */
224
+ targetDirFlag?: boolean;
225
+ /** Flags whose operand is NEVER a source: a short LETTER ('i') or a long name ('--exclude'). */
226
+ skipFlags?: string[];
227
+ }
228
+ declare const COPY_VERBS: Record<string, CopyShape>;
210
229
  interface FsOpVerdict {
211
230
  ruleName: string;
212
231
  verdict: 'block' | 'review';
@@ -253,6 +272,29 @@ declare function detectInlineExec(command: string): boolean;
253
272
  * the tool-managed cache allow-list. Used to gate `rm -rf` on home paths.
254
273
  */
255
274
  declare function isProtectedHomePath(rawPath: string): boolean;
275
+ /**
276
+ * Extract literal-text positional arguments from a CallExpr. Skips flags
277
+ * (anything starting with `-`) and ParamExp/CmdSubst (dynamic) parts. Returns
278
+ * the resolved string for each arg that is purely literal text.
279
+ */
280
+ /** A resolved, non-flag argument and where it sits. */
281
+ interface PositionedArg {
282
+ /** The resolved literal. */
283
+ value: string;
284
+ /** 0-based SLOT among non-flag words -- `cp SRC DEST`: SRC is 0, DEST is 1. */
285
+ index: number;
286
+ /** Absolute index in the resolved word list, for explainability. */
287
+ argv: number;
288
+ /** The flag immediately before this word (`ssh -i KEY`: '-i'), else null. */
289
+ afterFlag: string | null;
290
+ }
291
+ /**
292
+ * The positioned non-flag words of `words[from..to)`. A dynamic word (null)
293
+ * occupies no slot AND breaks the flag link -- `-v $SRC KEY` gives KEY no flag,
294
+ * because something unknowable sat between them. Its `.map(a => a.value)` is
295
+ * exactly the pre-stage-3 filter, which jail-position.spec.ts pins.
296
+ */
297
+ declare function positionedArgs(words: (string | null)[], from?: number, to?: number): PositionedArg[];
256
298
  interface ShellDestination {
257
299
  /** Extracted hostname, lowercased (e.g. "evil.com", "10.0.0.5"). */
258
300
  host: string;
@@ -295,6 +337,12 @@ interface ShellDestToken {
295
337
  */
296
338
  declare function extractShellDestTokens(command: string): ShellDestToken[];
297
339
  declare function analyzeFsOperation(command: string): FsOpVerdict | null;
340
+ /**
341
+ * One representative command per copy verb, for a gate test that DERIVES its
342
+ * rows from COPY_VERBS instead of keeping a second hand list (the trap the
343
+ * prescreen comment above describes). `src` is the path being copied out.
344
+ */
345
+ declare function sampleCopyCommand(verb: string, src: string): string;
298
346
  interface ShellCommandAnalysis {
299
347
  /** First word of every CallExpr — the command names invoked. */
300
348
  actions: string[];
@@ -1203,7 +1251,7 @@ declare const LONG_OUTPUT_THRESHOLD_BYTES: number;
1203
1251
  * and fails CI when the hash drifts without a version bump — forgetting
1204
1252
  * is loud, not silent.
1205
1253
  */
1206
- declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v13";
1254
+ declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
1207
1255
  /**
1208
1256
  * SHA-256 prefix of the detector-source files
1209
1257
  * (canonical.ts + pii.ts + destructive-regex.ts).
@@ -1214,7 +1262,7 @@ declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v13";
1214
1262
  * files changed, this hash must change too, and you must consciously
1215
1263
  * decide whether to bump CANONICAL_EXTRACTOR_VERSION."
1216
1264
  */
1217
- declare const CANONICAL_EXTRACTOR_HASH = "4622a2f41696af04";
1265
+ declare const CANONICAL_EXTRACTOR_HASH = "8b5729fe236a195b";
1218
1266
  declare function extractCanonicalFindings(call: ToolCallEntry, ctx: ExtractContext): CanonicalFinding[];
1219
1267
  declare function extractSessionLevelFindings(calls: ReadonlyArray<SessionToolCall>, ctx: SessionExtractContext): CanonicalFinding[];
1220
1268
  /**
@@ -1334,4 +1382,4 @@ declare function ssrfDestinationFloor(toolName: string, args: unknown, opts?: {
1334
1382
  /** Engine version stamped on audit entries for future drift detection. */
1335
1383
  declare const ENGINE_VERSION = "1.4.0";
1336
1384
 
1337
- export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COMMAND_WRAPPERS, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTINATION_ARGS, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, NET_BINARIES, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfFloor, ssrfReason, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, unwrapCommandHead, validateOverrides, validateRegex, validateShieldDefinition };
1385
+ export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COMMAND_WRAPPERS, COPY_VERBS, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTINATION_ARGS, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, NET_BINARIES, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type PositionedArg, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, positionedArgs, previewArgs, redactText, resolvePinned, sampleCopyCommand, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfFloor, ssrfReason, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, unwrapCommandHead, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.d.ts CHANGED
@@ -207,6 +207,25 @@ declare function detectDangerousShellExec(command: string): 'block' | 'review' |
207
207
  /** @deprecated Use detectDangerousShellExec — kept for backwards compatibility */
208
208
  declare const detectDangerousEval: typeof detectDangerousShellExec;
209
209
  declare const FS_READ_TOOLS: Set<string>;
210
+ /** How a copy verb's arguments are read. */
211
+ interface CopyShape {
212
+ /**
213
+ * allButLast cp SRC... DEST (GNU -t moves DEST into a flag: every slot is a source)
214
+ * first ln TARGET LINK
215
+ * all gzip -c FILE
216
+ * archive tar/zip/ar/7z: the inputs after the archive slot, in a WRITING mode
217
+ * flagOperand the source is a flag's operand: az ... upload -f SRC / --file SRC
218
+ */
219
+ source: 'allButLast' | 'first' | 'all' | 'archive' | 'flagOperand';
220
+ archive?: 'tar' | 'zip' | 'ar' | '7z';
221
+ /** flagOperand: the flags (short letter or long name) whose operand is the source. */
222
+ sourceFlags?: string[];
223
+ /** GNU `-t DIR` / `--target-directory`: the destination is in a flag. */
224
+ targetDirFlag?: boolean;
225
+ /** Flags whose operand is NEVER a source: a short LETTER ('i') or a long name ('--exclude'). */
226
+ skipFlags?: string[];
227
+ }
228
+ declare const COPY_VERBS: Record<string, CopyShape>;
210
229
  interface FsOpVerdict {
211
230
  ruleName: string;
212
231
  verdict: 'block' | 'review';
@@ -253,6 +272,29 @@ declare function detectInlineExec(command: string): boolean;
253
272
  * the tool-managed cache allow-list. Used to gate `rm -rf` on home paths.
254
273
  */
255
274
  declare function isProtectedHomePath(rawPath: string): boolean;
275
+ /**
276
+ * Extract literal-text positional arguments from a CallExpr. Skips flags
277
+ * (anything starting with `-`) and ParamExp/CmdSubst (dynamic) parts. Returns
278
+ * the resolved string for each arg that is purely literal text.
279
+ */
280
+ /** A resolved, non-flag argument and where it sits. */
281
+ interface PositionedArg {
282
+ /** The resolved literal. */
283
+ value: string;
284
+ /** 0-based SLOT among non-flag words -- `cp SRC DEST`: SRC is 0, DEST is 1. */
285
+ index: number;
286
+ /** Absolute index in the resolved word list, for explainability. */
287
+ argv: number;
288
+ /** The flag immediately before this word (`ssh -i KEY`: '-i'), else null. */
289
+ afterFlag: string | null;
290
+ }
291
+ /**
292
+ * The positioned non-flag words of `words[from..to)`. A dynamic word (null)
293
+ * occupies no slot AND breaks the flag link -- `-v $SRC KEY` gives KEY no flag,
294
+ * because something unknowable sat between them. Its `.map(a => a.value)` is
295
+ * exactly the pre-stage-3 filter, which jail-position.spec.ts pins.
296
+ */
297
+ declare function positionedArgs(words: (string | null)[], from?: number, to?: number): PositionedArg[];
256
298
  interface ShellDestination {
257
299
  /** Extracted hostname, lowercased (e.g. "evil.com", "10.0.0.5"). */
258
300
  host: string;
@@ -295,6 +337,12 @@ interface ShellDestToken {
295
337
  */
296
338
  declare function extractShellDestTokens(command: string): ShellDestToken[];
297
339
  declare function analyzeFsOperation(command: string): FsOpVerdict | null;
340
+ /**
341
+ * One representative command per copy verb, for a gate test that DERIVES its
342
+ * rows from COPY_VERBS instead of keeping a second hand list (the trap the
343
+ * prescreen comment above describes). `src` is the path being copied out.
344
+ */
345
+ declare function sampleCopyCommand(verb: string, src: string): string;
298
346
  interface ShellCommandAnalysis {
299
347
  /** First word of every CallExpr — the command names invoked. */
300
348
  actions: string[];
@@ -1203,7 +1251,7 @@ declare const LONG_OUTPUT_THRESHOLD_BYTES: number;
1203
1251
  * and fails CI when the hash drifts without a version bump — forgetting
1204
1252
  * is loud, not silent.
1205
1253
  */
1206
- declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v13";
1254
+ declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
1207
1255
  /**
1208
1256
  * SHA-256 prefix of the detector-source files
1209
1257
  * (canonical.ts + pii.ts + destructive-regex.ts).
@@ -1214,7 +1262,7 @@ declare const CANONICAL_EXTRACTOR_VERSION = "canonical-v13";
1214
1262
  * files changed, this hash must change too, and you must consciously
1215
1263
  * decide whether to bump CANONICAL_EXTRACTOR_VERSION."
1216
1264
  */
1217
- declare const CANONICAL_EXTRACTOR_HASH = "4622a2f41696af04";
1265
+ declare const CANONICAL_EXTRACTOR_HASH = "8b5729fe236a195b";
1218
1266
  declare function extractCanonicalFindings(call: ToolCallEntry, ctx: ExtractContext): CanonicalFinding[];
1219
1267
  declare function extractSessionLevelFindings(calls: ReadonlyArray<SessionToolCall>, ctx: SessionExtractContext): CanonicalFinding[];
1220
1268
  /**
@@ -1334,4 +1382,4 @@ declare function ssrfDestinationFloor(toolName: string, args: unknown, opts?: {
1334
1382
  /** Engine version stamped on audit entries for future drift detection. */
1335
1383
  declare const ENGINE_VERSION = "1.4.0";
1336
1384
 
1337
- export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COMMAND_WRAPPERS, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTINATION_ARGS, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, NET_BINARIES, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, previewArgs, redactText, resolvePinned, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfFloor, ssrfReason, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, unwrapCommandHead, validateOverrides, validateRegex, validateShieldDefinition };
1385
+ export { AST_FS_REGEX_RULES, type AuditEntryForClassify, BASH_TOOL_NAMES, BUILTIN_SHIELDS, type BlastEnvFinding, type BlastFinding, type BlastResult, type BlastSummary, CANARY_MIN_LENGTH, CANONICAL_EXTRACTOR_HASH, CANONICAL_EXTRACTOR_VERSION, COMMAND_WRAPPERS, COPY_VERBS, COST_PER_LOOP_ITER_USD, type CanaryHit, type CanaryValue, type CanaryView, type CanonicalAgent, type CanonicalFinding, type CanonicalFindingType, type CanonicalSourceType, DEFAULT_EGRESS_ALLOWLIST, DESTINATION_ARGS, DESTRUCTIVE_OP_RE, DLP_PATTERNS, DLP_SCAN_LIMITS, type DlpMatch, ENGINE_VERSION, type EgressPolicy, type EgressVerdict, type ExtractContext, FILE_TOOLS, FLAGS_WITH_VALUES, FS_READ_TOOLS, type FsOpVerdict, type InjectionConfidence, type InjectionContext, type InjectionMatch, LONG_OUTPUT_THRESHOLD_BYTES, LOOP_MAX_RECORDS, LOOP_THRESHOLD_FOR_WASTE, type LoopWindowEvaluation, NET_BINARIES, PRIVILEGE_ESCALATION_RE, type PiiPattern, type PipeChainAnalysis, type PolicyConfig, type PolicyContext, type PolicyHostHooks, type PolicyVerdict, type PositionedArg, type ProvenanceLookup, type ProvenanceTrust, REALTIME_PII_PATTERNS, type RiskMetadata, SCAN_SIGNAL_WEIGHTS, SENSITIVE_PATH_RE, SENSITIVE_PATH_REGEXES, SSRF_MAX_HOST, type ScanFinding, type ScanSignals, type ScanSummary, type ScoreTier, type SessionExtractContext, type SessionToolCall, type Severity, type ShellCommandAnalysis, type ShellDestToken, type ShellDestination, type ShieldDefinition, type ShieldOverrides, type ShieldVerdict, type SmartCondition, type SmartRule, type SsrfFloorOptions, type SsrfMatch, type SsrfTier, type SsrfVerdict, type ToolCallEntry, type ToolCallRecord, analyzeFsOperation, analyzePipeChain, analyzeShellCommand, checkDangerousSql, classifyAuditEntry, classifyRuleSeverity, classifyScanSignal, classifySsrf, computeAgentDeviceScore, computeArgsHash, computeBlendedSecurityScore, computeScanScore, computeSecurityScore, dedupeCanonicalFindings, detectArgsPii, detectDangerousEval, detectDangerousShellExec, detectInlineExec, detectPii, evaluateEgress, evaluateLoopWindow, evaluatePolicy, evaluateSmartConditions, extractAllSshHosts, extractCanonicalFindings, extractNetworkTargets, extractPositionalArgs, extractSessionLevelFindings, extractShellDestTokens, extractShellDestinations, getCompiledRegex, getNestedValue, hostMatches, isBashTool, isIgnoredTool, isPrivateHost, isProtectedHomePath, isShellShapedTool, isShieldVerdict, isStrictGatedTier, matchCanary, matchCanaryArgs, matchSensitivePath, matchesPattern, narrativeRuleLabel, normalizeCommandForPolicy, normalizeIpLiteral, parseAllSshHostsFromCommand, parseDestHost, positionedArgs, previewArgs, redactText, resolvePinned, sampleCopyCommand, scanArgs, scanInjection, scanText, sensitivePathMatch, ssrfDestinationFloor, ssrfFloor, ssrfReason, summarizeBlast, summarizeScan, toScanFinding, toolMatchesRule, truncateBlastPath, unwrapCommandHead, validateOverrides, validateRegex, validateShieldDefinition };
package/dist/index.js CHANGED
@@ -37,6 +37,7 @@ __export(src_exports, {
37
37
  CANONICAL_EXTRACTOR_HASH: () => CANONICAL_EXTRACTOR_HASH,
38
38
  CANONICAL_EXTRACTOR_VERSION: () => CANONICAL_EXTRACTOR_VERSION,
39
39
  COMMAND_WRAPPERS: () => COMMAND_WRAPPERS,
40
+ COPY_VERBS: () => COPY_VERBS,
40
41
  COST_PER_LOOP_ITER_USD: () => COST_PER_LOOP_ITER_USD,
41
42
  DEFAULT_EGRESS_ALLOWLIST: () => DEFAULT_EGRESS_ALLOWLIST,
42
43
  DESTINATION_ARGS: () => DESTINATION_ARGS,
@@ -106,9 +107,11 @@ __export(src_exports, {
106
107
  normalizeIpLiteral: () => normalizeIpLiteral,
107
108
  parseAllSshHostsFromCommand: () => parseAllSshHostsFromCommand,
108
109
  parseDestHost: () => parseDestHost,
110
+ positionedArgs: () => positionedArgs,
109
111
  previewArgs: () => previewArgs,
110
112
  redactText: () => redactText,
111
113
  resolvePinned: () => resolvePinned,
114
+ sampleCopyCommand: () => sampleCopyCommand,
112
115
  scanArgs: () => scanArgs,
113
116
  scanInjection: () => scanInjection,
114
117
  scanText: () => scanText,
@@ -1442,12 +1445,56 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1442
1445
  "nl",
1443
1446
  "dd"
1444
1447
  ]);
1448
+ var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
1449
+ var RSYNC_SKIP = [
1450
+ "e",
1451
+ "--rsh",
1452
+ "--exclude",
1453
+ "--exclude-from",
1454
+ "--include",
1455
+ "--include-from",
1456
+ "--files-from",
1457
+ "f",
1458
+ "--filter"
1459
+ ];
1460
+ var COPY_VERBS = {
1461
+ cp: { source: "allButLast", targetDirFlag: true },
1462
+ mv: { source: "allButLast", targetDirFlag: true },
1463
+ install: { source: "allButLast", targetDirFlag: true },
1464
+ ln: { source: "first", targetDirFlag: true },
1465
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
1466
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
1467
+ tar: {
1468
+ source: "archive",
1469
+ archive: "tar",
1470
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
1471
+ },
1472
+ zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
1473
+ ar: { source: "archive", archive: "ar" },
1474
+ "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
1475
+ gzip: { source: "all" },
1476
+ bzip2: { source: "all" },
1477
+ xz: { source: "all" },
1478
+ "docker cp": { source: "allButLast" },
1479
+ "kubectl cp": { source: "allButLast" },
1480
+ "gsutil cp": { source: "allButLast" },
1481
+ "gsutil rsync": { source: "allButLast" },
1482
+ "rclone copy": { source: "allButLast" },
1483
+ "rclone sync": { source: "allButLast" },
1484
+ "aws s3 cp": { source: "allButLast" },
1485
+ "aws s3 mv": { source: "allButLast" },
1486
+ "aws s3 sync": { source: "allButLast" },
1487
+ "gcloud storage cp": { source: "allButLast" },
1488
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
1489
+ };
1490
+ var TAR_MODE_WORD = /^[a-zA-Z]+$/;
1491
+ var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
1445
1492
  var FS_OP_PRESCREEN_RE = new RegExp(
1446
1493
  // A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
1447
1494
  // reader right after `"` / `'`, and without these two characters the
1448
1495
  // prescreen rejected every string-wrapped read before the parser ran.
1449
1496
  // Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
1450
- `(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
1497
+ `(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
1451
1498
  );
1452
1499
  var HOME_CACHE_ALLOWLIST = [
1453
1500
  ".cache",
@@ -1882,20 +1929,32 @@ function isProtectedHomePath(rawPath) {
1882
1929
  }
1883
1930
  return true;
1884
1931
  }
1885
- function extractLiteralArgs(callExpr) {
1886
- const args = callExpr.Args || [];
1887
- if (args.length === 0) return { name: "", flags: [], paths: [], words: [] };
1888
- const words = args.map((a) => resolveWordLiteral(a));
1889
- const name = (words[0] ?? "").toLowerCase();
1890
- const flags = [];
1891
- const paths = [];
1892
- for (let i = 1; i < words.length; i++) {
1932
+ function positionedArgs(words, from = 1, to = words.length) {
1933
+ const out = [];
1934
+ let afterFlag = null;
1935
+ for (let i = from; i < to; i++) {
1893
1936
  const v = words[i];
1894
- if (v === null) continue;
1895
- if (v.startsWith("-")) flags.push(v);
1896
- else paths.push(v);
1937
+ if (v === null) {
1938
+ afterFlag = null;
1939
+ continue;
1940
+ }
1941
+ if (v.startsWith("-")) {
1942
+ afterFlag = v;
1943
+ continue;
1944
+ }
1945
+ out.push({ value: v, index: out.length, argv: i, afterFlag });
1946
+ afterFlag = null;
1897
1947
  }
1898
- return { name, flags, paths, words };
1948
+ return out;
1949
+ }
1950
+ function extractLiteralArgs(callExpr) {
1951
+ const rawArgs = callExpr.Args || [];
1952
+ if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
1953
+ const words = rawArgs.map((a) => resolveWordLiteral(a));
1954
+ const name = baseWord(words[0]);
1955
+ const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
1956
+ const args = positionedArgs(words);
1957
+ return { name, flags, paths: args.map((a) => a.value), words, args };
1899
1958
  }
1900
1959
  var NET_BINARIES = /* @__PURE__ */ new Set([
1901
1960
  "curl",
@@ -2319,6 +2378,9 @@ function analyzeFsOperationImpl(command, depth = 0) {
2319
2378
  if (result?.verdict === "block") return false;
2320
2379
  }
2321
2380
  }
2381
+ for (const p of copySourcePaths(words)) {
2382
+ result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
2383
+ }
2322
2384
  return true;
2323
2385
  });
2324
2386
  return result;
@@ -2331,6 +2393,157 @@ function stricter(a, b) {
2331
2393
  if (!b) return a;
2332
2394
  return b.verdict === "block" && a.verdict !== "block" ? b : a;
2333
2395
  }
2396
+ function flagInfo(w) {
2397
+ if (w.startsWith("--")) {
2398
+ const eq = w.indexOf("=");
2399
+ return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
2400
+ }
2401
+ const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
2402
+ if (!m) return { letter: null, long: null, attached: null };
2403
+ return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
2404
+ }
2405
+ function flagIs(w, names) {
2406
+ if (w === null) return false;
2407
+ const f = flagInfo(w);
2408
+ return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
2409
+ }
2410
+ function operandOf(a, names) {
2411
+ if (!names || a.afterFlag === null) return false;
2412
+ return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
2413
+ }
2414
+ function resolveCopyShape(words, h) {
2415
+ const verb = baseWord(words[h]);
2416
+ if (!verb) return null;
2417
+ const direct = COPY_VERBS[verb];
2418
+ if (direct) return { shape: direct, last: h };
2419
+ const slots = positionedArgs(words, h + 1);
2420
+ for (let i = 0; i < slots.length; i++) {
2421
+ for (let n = 3; n >= 1; n--) {
2422
+ const part = slots.slice(i, i + n);
2423
+ if (part.length < n) continue;
2424
+ const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
2425
+ const shape = COPY_VERBS[key];
2426
+ if (shape) return { shape, last: part[n - 1].argv };
2427
+ }
2428
+ if (slots[i].afterFlag === null) return null;
2429
+ }
2430
+ return null;
2431
+ }
2432
+ var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
2433
+ function findStartPoints(words, h) {
2434
+ const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
2435
+ if (k < 0) return { k, starts: [] };
2436
+ const firstPredicate = words.findIndex(
2437
+ (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
2438
+ );
2439
+ const end = firstPredicate > h ? firstPredicate : k;
2440
+ return { k, starts: positionalAfter(words, h + 1, end) };
2441
+ }
2442
+ function copySourcePaths(words) {
2443
+ const h = unwrapCommandHead(words);
2444
+ const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
2445
+ if (fi >= 0) {
2446
+ const { k, starts } = findStartPoints(words, fi);
2447
+ if (k < 0) return [];
2448
+ const action = unwrapCommandHead(words.slice(k + 1));
2449
+ return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
2450
+ }
2451
+ if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
2452
+ const r = resolveCopyShape(words, h);
2453
+ if (!r) return [];
2454
+ const { shape, last } = r;
2455
+ const args = positionedArgs(words, last + 1);
2456
+ const tail = words.slice(last + 1);
2457
+ const skipped = (a) => operandOf(a, shape.skipFlags);
2458
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
2459
+ const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
2460
+ const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
2461
+ const dynamicDest = lastOperand === null;
2462
+ const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
2463
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
2464
+ return [];
2465
+ let src;
2466
+ switch (shape.source) {
2467
+ case "all":
2468
+ src = args;
2469
+ break;
2470
+ case "first":
2471
+ src = targetDir ? args : args.slice(0, 1);
2472
+ break;
2473
+ case "flagOperand": {
2474
+ const inline = tail.filter((w) => w !== null && w.startsWith("--")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && (shape.sourceFlags ?? []).includes(f.long ?? "")).map((f) => f.attached);
2475
+ return [
2476
+ ...args.filter(
2477
+ (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
2478
+ ).map((a) => a.value),
2479
+ ...inline
2480
+ ];
2481
+ }
2482
+ case "archive":
2483
+ src = archiveInputs(shape.archive, args, tail);
2484
+ break;
2485
+ case "allButLast":
2486
+ src = targetDir || dynamicDest ? args : args.slice(0, -1);
2487
+ break;
2488
+ }
2489
+ return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
2490
+ }
2491
+ function archiveInputs(kind, args, tail) {
2492
+ const first = args[0];
2493
+ const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
2494
+ if (kind === "tar") {
2495
+ const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
2496
+ const mode = (bareKey ? first.value : "") + flagsText;
2497
+ const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
2498
+ const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
2499
+ if (extracting && !writing) return [];
2500
+ void mode;
2501
+ let i = 0;
2502
+ if (bareKey) {
2503
+ i = 1;
2504
+ const next = args[1];
2505
+ if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
2506
+ }
2507
+ return args.slice(i);
2508
+ }
2509
+ if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
2510
+ if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
2511
+ return args.slice(2);
2512
+ }
2513
+ function sampleCopyCommand(verb, src) {
2514
+ const shape = COPY_VERBS[verb];
2515
+ if (!shape) throw new Error(`not a copy verb: ${verb}`);
2516
+ const remote = /^(scp|rsync|aws |gsutil|gcloud|rclone|docker|kubectl)/.test(verb);
2517
+ switch (shape.source) {
2518
+ case "first":
2519
+ return `${verb} -s ${src} /tmp/n9-link`;
2520
+ case "all":
2521
+ return `${verb} -c ${src} > /tmp/n9-out`;
2522
+ case "flagOperand":
2523
+ return `${verb} -f ${src} -c n9`;
2524
+ case "archive":
2525
+ return shape.archive === "tar" ? `tar czf /tmp/n9-out.tgz ${src}` : shape.archive === "zip" ? `zip -r /tmp/n9-out.zip ${src}` : shape.archive === "ar" ? `ar rc /tmp/n9-out.a ${src}` : `7z a /tmp/n9-out.7z ${src}`;
2526
+ case "allButLast":
2527
+ return `${verb} ${src} ${remote ? verb.startsWith("scp") || verb === "rsync" ? "user@host.invalid:/tmp/" : verb.startsWith("docker") || verb.startsWith("kubectl") ? "ctr:/tmp/" : "remote:/n9/" : "/tmp/n9-copy"}`;
2528
+ }
2529
+ }
2530
+ var COPY_RULE_OF = {
2531
+ "shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
2532
+ "shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
2533
+ "shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
2534
+ "shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
2535
+ };
2536
+ function copyVerdictOf(hit) {
2537
+ if (!hit) return null;
2538
+ const ruleName = COPY_RULE_OF[hit.ruleName];
2539
+ if (!ruleName) return null;
2540
+ return {
2541
+ ruleName,
2542
+ verdict: "review",
2543
+ reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
2544
+ path: hit.path
2545
+ };
2546
+ }
2334
2547
  function matchSensitivePath2(p) {
2335
2548
  for (const sp of SENSITIVE_PATH_RULES) {
2336
2549
  if (sp.match(p))
@@ -2338,14 +2551,15 @@ function matchSensitivePath2(p) {
2338
2551
  }
2339
2552
  return null;
2340
2553
  }
2341
- var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(w.split("/").pop()?.toLowerCase() ?? "");
2342
- var positionalAfter = (words, from, to = words.length) => words.slice(from, to).filter((w) => w !== null && !w.startsWith("-"));
2554
+ function baseWord(w) {
2555
+ return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
2556
+ }
2557
+ var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
2558
+ var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
2343
2559
  function wrappedReadPaths(words, name) {
2344
2560
  if (name === "find") {
2345
- const k = words.findIndex((w) => w !== null && FIND_EXEC_FLAGS.has(w));
2346
- if (k < 1 || !isReaderWord(words[k + 1] ?? null)) return null;
2347
- const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
2348
- return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
2561
+ const { k, starts } = findStartPoints(words, 0);
2562
+ return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
2349
2563
  }
2350
2564
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
2351
2565
  const h = unwrapCommandHead(words);
@@ -3233,6 +3447,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3233
3447
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
3234
3448
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
3235
3449
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
3450
+ let pendingAstReview;
3236
3451
  if (bashCommand !== null) {
3237
3452
  const pipeVerdict = pipeChainVerdict(
3238
3453
  bashCommand,
@@ -3244,7 +3459,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3244
3459
  if (fsVerdict) {
3245
3460
  const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
3246
3461
  const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
3247
- return {
3462
+ const astVerdict = {
3248
3463
  decision: fsVerdict.verdict,
3249
3464
  blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
3250
3465
  reason: fsVerdict.reason,
@@ -3252,6 +3467,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3252
3467
  ruleName: fsVerdict.ruleName,
3253
3468
  ruleDescription: fsVerdict.reason
3254
3469
  };
3470
+ if (fsVerdict.verdict === "block") return astVerdict;
3471
+ pendingAstReview = astVerdict;
3255
3472
  }
3256
3473
  const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
3257
3474
  const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
@@ -3294,7 +3511,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3294
3511
  const matchedRule = resolvePinned(matches);
3295
3512
  if (matchedRule) {
3296
3513
  if (matchedRule.verdict === "allow")
3297
- return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
3514
+ return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
3298
3515
  return {
3299
3516
  decision: matchedRule.verdict,
3300
3517
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -3321,6 +3538,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3321
3538
  allTokens = analyzed.allTokens;
3322
3539
  pathTokens = analyzed.paths;
3323
3540
  const candidates = [];
3541
+ if (pendingAstReview) candidates.push(pendingAstReview);
3324
3542
  const evalVerdict = detectDangerousShellExec(shellCommand);
3325
3543
  if (evalVerdict === "block") {
3326
3544
  return {
@@ -4427,6 +4645,12 @@ function classifyRuleSeverity(name, verdict) {
4427
4645
  "read-ssh",
4428
4646
  "read-gcp",
4429
4647
  "read-cred",
4648
+ // Stage 4 (2026-09-11): a copy of a credential file scores like a READ of it --
4649
+ // read-ssh/aws/cred are critical, so copy-ssh/aws/cred are; read-env is high
4650
+ // (below), so copy-env joins the high list, not this one (/code-review).
4651
+ "copy-ssh",
4652
+ "copy-aws",
4653
+ "copy-cred",
4430
4654
  "delete-repo",
4431
4655
  "helm-uninstall",
4432
4656
  "drop-table",
@@ -4439,6 +4663,7 @@ function classifyRuleSeverity(name, verdict) {
4439
4663
  ];
4440
4664
  if (criticalPatterns.some((p) => n.includes(p))) return "critical";
4441
4665
  const highPatterns = [
4666
+ "copy-env",
4442
4667
  "force-push",
4443
4668
  "force_push",
4444
4669
  "git-destructive",
@@ -4456,6 +4681,11 @@ function narrativeRuleLabel(name) {
4456
4681
  const map = {
4457
4682
  "read-aws": "AWS credentials read",
4458
4683
  "read-ssh": "SSH private key read",
4684
+ // Stage 4 copy twins, so `scan --narrative` prints a label, not a raw slug.
4685
+ "copy-ssh": "SSH private key copied out",
4686
+ "copy-aws": "AWS credentials copied out",
4687
+ "copy-env": ".env file copied out",
4688
+ "copy-cred": "credential file copied out",
4459
4689
  "read-gcp": "GCP credentials read",
4460
4690
  "read-cred": "credential file read",
4461
4691
  "delete-repo": "GitHub repository deletion",
@@ -4853,8 +5083,8 @@ function matchCanaryArgs(args, values) {
4853
5083
 
4854
5084
  // src/scan/canonical.ts
4855
5085
  var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
4856
- var CANONICAL_EXTRACTOR_VERSION = "canonical-v13";
4857
- var CANONICAL_EXTRACTOR_HASH = "4622a2f41696af04";
5086
+ var CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
5087
+ var CANONICAL_EXTRACTOR_HASH = "8b5729fe236a195b";
4858
5088
  var DEDUPE_PREVIEW_LEN = 120;
4859
5089
  function extractCanonicalFindings(call, ctx) {
4860
5090
  const out = [];
@@ -5231,6 +5461,7 @@ var ENGINE_VERSION = "1.4.0";
5231
5461
  CANONICAL_EXTRACTOR_HASH,
5232
5462
  CANONICAL_EXTRACTOR_VERSION,
5233
5463
  COMMAND_WRAPPERS,
5464
+ COPY_VERBS,
5234
5465
  COST_PER_LOOP_ITER_USD,
5235
5466
  DEFAULT_EGRESS_ALLOWLIST,
5236
5467
  DESTINATION_ARGS,
@@ -5300,9 +5531,11 @@ var ENGINE_VERSION = "1.4.0";
5300
5531
  normalizeIpLiteral,
5301
5532
  parseAllSshHostsFromCommand,
5302
5533
  parseDestHost,
5534
+ positionedArgs,
5303
5535
  previewArgs,
5304
5536
  redactText,
5305
5537
  resolvePinned,
5538
+ sampleCopyCommand,
5306
5539
  scanArgs,
5307
5540
  scanInjection,
5308
5541
  scanText,
package/dist/index.mjs CHANGED
@@ -1312,12 +1312,56 @@ var FS_READ_TOOLS = /* @__PURE__ */ new Set([
1312
1312
  "nl",
1313
1313
  "dd"
1314
1314
  ]);
1315
+ var SCP_VALUE_FLAGS = ["i", "F", "o", "c", "S", "P", "J", "D", "W", "l"];
1316
+ var RSYNC_SKIP = [
1317
+ "e",
1318
+ "--rsh",
1319
+ "--exclude",
1320
+ "--exclude-from",
1321
+ "--include",
1322
+ "--include-from",
1323
+ "--files-from",
1324
+ "f",
1325
+ "--filter"
1326
+ ];
1327
+ var COPY_VERBS = {
1328
+ cp: { source: "allButLast", targetDirFlag: true },
1329
+ mv: { source: "allButLast", targetDirFlag: true },
1330
+ install: { source: "allButLast", targetDirFlag: true },
1331
+ ln: { source: "first", targetDirFlag: true },
1332
+ scp: { source: "allButLast", skipFlags: SCP_VALUE_FLAGS },
1333
+ rsync: { source: "allButLast", skipFlags: RSYNC_SKIP },
1334
+ tar: {
1335
+ source: "archive",
1336
+ archive: "tar",
1337
+ skipFlags: ["f", "X", "T", "--file", "--exclude", "--exclude-from", "--files-from"]
1338
+ },
1339
+ zip: { source: "archive", archive: "zip", skipFlags: ["x", "i", "--exclude", "--include"] },
1340
+ ar: { source: "archive", archive: "ar" },
1341
+ "7z": { source: "archive", archive: "7z", skipFlags: ["x", "--exclude"] },
1342
+ gzip: { source: "all" },
1343
+ bzip2: { source: "all" },
1344
+ xz: { source: "all" },
1345
+ "docker cp": { source: "allButLast" },
1346
+ "kubectl cp": { source: "allButLast" },
1347
+ "gsutil cp": { source: "allButLast" },
1348
+ "gsutil rsync": { source: "allButLast" },
1349
+ "rclone copy": { source: "allButLast" },
1350
+ "rclone sync": { source: "allButLast" },
1351
+ "aws s3 cp": { source: "allButLast" },
1352
+ "aws s3 mv": { source: "allButLast" },
1353
+ "aws s3 sync": { source: "allButLast" },
1354
+ "gcloud storage cp": { source: "allButLast" },
1355
+ "az storage blob upload": { source: "flagOperand", sourceFlags: ["f", "--file"] }
1356
+ };
1357
+ var TAR_MODE_WORD = /^[a-zA-Z]+$/;
1358
+ var COPY_VERB_HEADS = new Set(Object.keys(COPY_VERBS).map((k) => k.split(" ")[0]));
1315
1359
  var FS_OP_PRESCREEN_RE = new RegExp(
1316
1360
  // A quote is a separator too: `eval "cat X"` and `sh -c 'cat X'` put the
1317
1361
  // reader right after `"` / `'`, and without these two characters the
1318
1362
  // prescreen rejected every string-wrapped read before the parser ran.
1319
1363
  // Found 2026-09-11 by instrumenting the walk -- no CallExpr was ever visited.
1320
- `(?:^|[\\s|;&("'\`\\n])(?:rm|${[...FS_READ_TOOLS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
1364
+ `(?:^|[\\s|;&("'\`\\n/])(?:rm|${[...FS_READ_TOOLS, ...COPY_VERB_HEADS].map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b|(?<!<)<(?!<)`
1321
1365
  );
1322
1366
  var HOME_CACHE_ALLOWLIST = [
1323
1367
  ".cache",
@@ -1752,20 +1796,32 @@ function isProtectedHomePath(rawPath) {
1752
1796
  }
1753
1797
  return true;
1754
1798
  }
1755
- function extractLiteralArgs(callExpr) {
1756
- const args = callExpr.Args || [];
1757
- if (args.length === 0) return { name: "", flags: [], paths: [], words: [] };
1758
- const words = args.map((a) => resolveWordLiteral(a));
1759
- const name = (words[0] ?? "").toLowerCase();
1760
- const flags = [];
1761
- const paths = [];
1762
- for (let i = 1; i < words.length; i++) {
1799
+ function positionedArgs(words, from = 1, to = words.length) {
1800
+ const out = [];
1801
+ let afterFlag = null;
1802
+ for (let i = from; i < to; i++) {
1763
1803
  const v = words[i];
1764
- if (v === null) continue;
1765
- if (v.startsWith("-")) flags.push(v);
1766
- else paths.push(v);
1804
+ if (v === null) {
1805
+ afterFlag = null;
1806
+ continue;
1807
+ }
1808
+ if (v.startsWith("-")) {
1809
+ afterFlag = v;
1810
+ continue;
1811
+ }
1812
+ out.push({ value: v, index: out.length, argv: i, afterFlag });
1813
+ afterFlag = null;
1767
1814
  }
1768
- return { name, flags, paths, words };
1815
+ return out;
1816
+ }
1817
+ function extractLiteralArgs(callExpr) {
1818
+ const rawArgs = callExpr.Args || [];
1819
+ if (rawArgs.length === 0) return { name: "", flags: [], paths: [], words: [], args: [] };
1820
+ const words = rawArgs.map((a) => resolveWordLiteral(a));
1821
+ const name = baseWord(words[0]);
1822
+ const flags = words.slice(1).filter((w) => w !== null && w.startsWith("-"));
1823
+ const args = positionedArgs(words);
1824
+ return { name, flags, paths: args.map((a) => a.value), words, args };
1769
1825
  }
1770
1826
  var NET_BINARIES = /* @__PURE__ */ new Set([
1771
1827
  "curl",
@@ -2189,6 +2245,9 @@ function analyzeFsOperationImpl(command, depth = 0) {
2189
2245
  if (result?.verdict === "block") return false;
2190
2246
  }
2191
2247
  }
2248
+ for (const p of copySourcePaths(words)) {
2249
+ result = stricter(result, copyVerdictOf(matchSensitivePath2(p)));
2250
+ }
2192
2251
  return true;
2193
2252
  });
2194
2253
  return result;
@@ -2201,6 +2260,157 @@ function stricter(a, b) {
2201
2260
  if (!b) return a;
2202
2261
  return b.verdict === "block" && a.verdict !== "block" ? b : a;
2203
2262
  }
2263
+ function flagInfo(w) {
2264
+ if (w.startsWith("--")) {
2265
+ const eq = w.indexOf("=");
2266
+ return eq < 0 ? { letter: null, long: w, attached: null } : { letter: null, long: w.slice(0, eq), attached: w.slice(eq + 1) };
2267
+ }
2268
+ const m = /^-([a-zA-Z]+)(.*)$/.exec(w);
2269
+ if (!m) return { letter: null, long: null, attached: null };
2270
+ return { letter: m[1][m[1].length - 1], long: null, attached: m[2] === "" ? null : m[2] };
2271
+ }
2272
+ function flagIs(w, names) {
2273
+ if (w === null) return false;
2274
+ const f = flagInfo(w);
2275
+ return names.some((n) => n.startsWith("--") ? f.long === n : f.letter === n);
2276
+ }
2277
+ function operandOf(a, names) {
2278
+ if (!names || a.afterFlag === null) return false;
2279
+ return flagIs(a.afterFlag, names) && flagInfo(a.afterFlag).attached === null;
2280
+ }
2281
+ function resolveCopyShape(words, h) {
2282
+ const verb = baseWord(words[h]);
2283
+ if (!verb) return null;
2284
+ const direct = COPY_VERBS[verb];
2285
+ if (direct) return { shape: direct, last: h };
2286
+ const slots = positionedArgs(words, h + 1);
2287
+ for (let i = 0; i < slots.length; i++) {
2288
+ for (let n = 3; n >= 1; n--) {
2289
+ const part = slots.slice(i, i + n);
2290
+ if (part.length < n) continue;
2291
+ const key = [verb, ...part.map((a) => a.value.toLowerCase())].join(" ");
2292
+ const shape = COPY_VERBS[key];
2293
+ if (shape) return { shape, last: part[n - 1].argv };
2294
+ }
2295
+ if (slots[i].afterFlag === null) return null;
2296
+ }
2297
+ return null;
2298
+ }
2299
+ var FIND_OPTIONS = /* @__PURE__ */ new Set(["-H", "-L", "-P"]);
2300
+ function findStartPoints(words, h) {
2301
+ const k = words.findIndex((w, i) => i > h && w !== null && FIND_EXEC_FLAGS.has(w));
2302
+ if (k < 0) return { k, starts: [] };
2303
+ const firstPredicate = words.findIndex(
2304
+ (w, i) => i > h && w !== null && w.startsWith("-") && !FIND_OPTIONS.has(w)
2305
+ );
2306
+ const end = firstPredicate > h ? firstPredicate : k;
2307
+ return { k, starts: positionalAfter(words, h + 1, end) };
2308
+ }
2309
+ function copySourcePaths(words) {
2310
+ const h = unwrapCommandHead(words);
2311
+ const fi = words.findIndex((w, i) => i <= h && baseWord(w) === "find");
2312
+ if (fi >= 0) {
2313
+ const { k, starts } = findStartPoints(words, fi);
2314
+ if (k < 0) return [];
2315
+ const action = unwrapCommandHead(words.slice(k + 1));
2316
+ return resolveCopyShape(words.slice(k + 1), action) ? starts : [];
2317
+ }
2318
+ if (!COPY_VERB_HEADS.has(baseWord(words[h]))) return [];
2319
+ const r = resolveCopyShape(words, h);
2320
+ if (!r) return [];
2321
+ const { shape, last } = r;
2322
+ const args = positionedArgs(words, last + 1);
2323
+ const tail = words.slice(last + 1);
2324
+ const skipped = (a) => operandOf(a, shape.skipFlags);
2325
+ const targetDir = shape.targetDirFlag === true && tail.some((w) => w !== null && w.startsWith("-") && flagIs(w, ["t", "--target-directory"]));
2326
+ const targetOperand = (a) => targetDir && operandOf(a, ["t", "--target-directory"]);
2327
+ const lastOperand = [...tail].reverse().find((w) => w === null || !w.startsWith("-"));
2328
+ const dynamicDest = lastOperand === null;
2329
+ const destIsLastOperand = (shape.source === "allButLast" || shape.source === "first") && !targetDir && !dynamicDest;
2330
+ if (destIsLastOperand && typeof lastOperand === "string" && matchSensitivePath2(lastOperand))
2331
+ return [];
2332
+ let src;
2333
+ switch (shape.source) {
2334
+ case "all":
2335
+ src = args;
2336
+ break;
2337
+ case "first":
2338
+ src = targetDir ? args : args.slice(0, 1);
2339
+ break;
2340
+ case "flagOperand": {
2341
+ const inline = tail.filter((w) => w !== null && w.startsWith("--")).map((w) => flagInfo(w)).filter((f) => f.attached !== null && (shape.sourceFlags ?? []).includes(f.long ?? "")).map((f) => f.attached);
2342
+ return [
2343
+ ...args.filter(
2344
+ (a) => flagIs(a.afterFlag, shape.sourceFlags ?? []) && flagInfo(a.afterFlag).attached === null
2345
+ ).map((a) => a.value),
2346
+ ...inline
2347
+ ];
2348
+ }
2349
+ case "archive":
2350
+ src = archiveInputs(shape.archive, args, tail);
2351
+ break;
2352
+ case "allButLast":
2353
+ src = targetDir || dynamicDest ? args : args.slice(0, -1);
2354
+ break;
2355
+ }
2356
+ return src.filter((a) => !skipped(a) && !targetOperand(a)).map((a) => a.value);
2357
+ }
2358
+ function archiveInputs(kind, args, tail) {
2359
+ const first = args[0];
2360
+ const bareKey = first && first.afterFlag === null && TAR_MODE_WORD.test(first.value);
2361
+ if (kind === "tar") {
2362
+ const flagsText = tail.filter((w) => w !== null && w.startsWith("-")).join(" ");
2363
+ const mode = (bareKey ? first.value : "") + flagsText;
2364
+ const extracting = /x|t/.test(bareKey ? first.value.replace(/f/g, "") : "") || /(^|\s)-[a-zA-Z]*[xt]|--extract|--list|--get/.test(flagsText);
2365
+ const writing = /[cruA]/.test(bareKey ? first.value : "") || /(^|\s)-[a-zA-Z]*[cruA]|--create|--append|--update|--concatenate/.test(flagsText);
2366
+ if (extracting && !writing) return [];
2367
+ void mode;
2368
+ let i = 0;
2369
+ if (bareKey) {
2370
+ i = 1;
2371
+ const next = args[1];
2372
+ if (first.value.includes("f") && next && next.afterFlag === null) i = 2;
2373
+ }
2374
+ return args.slice(i);
2375
+ }
2376
+ if (kind === "zip") return first && first.afterFlag === "-" ? args : args.slice(1);
2377
+ if (kind === "ar") return bareKey ? args.slice(2) : args.slice(1);
2378
+ return args.slice(2);
2379
+ }
2380
+ function sampleCopyCommand(verb, src) {
2381
+ const shape = COPY_VERBS[verb];
2382
+ if (!shape) throw new Error(`not a copy verb: ${verb}`);
2383
+ const remote = /^(scp|rsync|aws |gsutil|gcloud|rclone|docker|kubectl)/.test(verb);
2384
+ switch (shape.source) {
2385
+ case "first":
2386
+ return `${verb} -s ${src} /tmp/n9-link`;
2387
+ case "all":
2388
+ return `${verb} -c ${src} > /tmp/n9-out`;
2389
+ case "flagOperand":
2390
+ return `${verb} -f ${src} -c n9`;
2391
+ case "archive":
2392
+ return shape.archive === "tar" ? `tar czf /tmp/n9-out.tgz ${src}` : shape.archive === "zip" ? `zip -r /tmp/n9-out.zip ${src}` : shape.archive === "ar" ? `ar rc /tmp/n9-out.a ${src}` : `7z a /tmp/n9-out.7z ${src}`;
2393
+ case "allButLast":
2394
+ return `${verb} ${src} ${remote ? verb.startsWith("scp") || verb === "rsync" ? "user@host.invalid:/tmp/" : verb.startsWith("docker") || verb.startsWith("kubectl") ? "ctr:/tmp/" : "remote:/n9/" : "/tmp/n9-copy"}`;
2395
+ }
2396
+ }
2397
+ var COPY_RULE_OF = {
2398
+ "shield:project-jail:block-read-ssh": "shield:project-jail:review-copy-ssh",
2399
+ "shield:project-jail:block-read-aws": "shield:project-jail:review-copy-aws",
2400
+ "shield:project-jail:block-read-env": "shield:project-jail:review-copy-env",
2401
+ "shield:project-jail:review-read-credentials": "shield:project-jail:review-copy-credentials"
2402
+ };
2403
+ function copyVerdictOf(hit) {
2404
+ if (!hit) return null;
2405
+ const ruleName = COPY_RULE_OF[hit.ruleName];
2406
+ if (!ruleName) return null;
2407
+ return {
2408
+ ruleName,
2409
+ verdict: "review",
2410
+ reason: `Copying ${hit.path} moves a credential out of its jail (project-jail shield)`,
2411
+ path: hit.path
2412
+ };
2413
+ }
2204
2414
  function matchSensitivePath2(p) {
2205
2415
  for (const sp of SENSITIVE_PATH_RULES) {
2206
2416
  if (sp.match(p))
@@ -2208,14 +2418,15 @@ function matchSensitivePath2(p) {
2208
2418
  }
2209
2419
  return null;
2210
2420
  }
2211
- var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(w.split("/").pop()?.toLowerCase() ?? "");
2212
- var positionalAfter = (words, from, to = words.length) => words.slice(from, to).filter((w) => w !== null && !w.startsWith("-"));
2421
+ function baseWord(w) {
2422
+ return (w ?? "").split("/").pop()?.toLowerCase() ?? "";
2423
+ }
2424
+ var isReaderWord = (w) => w !== null && FS_READ_TOOLS.has(baseWord(w));
2425
+ var positionalAfter = (words, from, to = words.length) => positionedArgs(words, from, to).map((a) => a.value);
2213
2426
  function wrappedReadPaths(words, name) {
2214
2427
  if (name === "find") {
2215
- const k = words.findIndex((w) => w !== null && FIND_EXEC_FLAGS.has(w));
2216
- if (k < 1 || !isReaderWord(words[k + 1] ?? null)) return null;
2217
- const firstPredicate = words.findIndex((w, i) => i > 0 && w !== null && w.startsWith("-"));
2218
- return positionalAfter(words, 1, firstPredicate > 0 ? firstPredicate : k);
2428
+ const { k, starts } = findStartPoints(words, 0);
2429
+ return k > 0 && isReaderWord(words[k + 1] ?? null) ? starts : null;
2219
2430
  }
2220
2431
  if (!COMMAND_WRAPPERS.has(name) && !RUNNER_WRAPPERS.has(name)) return null;
2221
2432
  const h = unwrapCommandHead(words);
@@ -3103,6 +3314,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3103
3314
  const shellShaped = isBashTool(toolName) || inspectsShellCommand(toolName, config.policy.toolInspection);
3104
3315
  const shellShapedCommand = shellShaped ? isBashTool(toolName) && args && typeof args === "object" ? typeof args.command === "string" ? args.command : null : extractShellCommand(toolName, args, config.policy.toolInspection) : null;
3105
3316
  const bashCommand = agent !== "Terminal" ? shellShapedCommand : null;
3317
+ let pendingAstReview;
3106
3318
  if (bashCommand !== null) {
3107
3319
  const pipeVerdict = pipeChainVerdict(
3108
3320
  bashCommand,
@@ -3114,7 +3326,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3114
3326
  if (fsVerdict) {
3115
3327
  const isShieldRule = fsVerdict.ruleName.startsWith("shield:");
3116
3328
  const labelPrefix = isShieldRule ? "project-jail (AST)" : "Node9 (AST)";
3117
- return {
3329
+ const astVerdict = {
3118
3330
  decision: fsVerdict.verdict,
3119
3331
  blockedByLabel: `${labelPrefix}: ${fsVerdict.ruleName}`,
3120
3332
  reason: fsVerdict.reason,
@@ -3122,6 +3334,8 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3122
3334
  ruleName: fsVerdict.ruleName,
3123
3335
  ruleDescription: fsVerdict.reason
3124
3336
  };
3337
+ if (fsVerdict.verdict === "block") return astVerdict;
3338
+ pendingAstReview = astVerdict;
3125
3339
  }
3126
3340
  const sqlAction = resolveCheck(config.policy.commandChecks?.sqlDdl);
3127
3341
  const sqlVerdict = sqlAction === "off" ? null : analyzeSqlDestructive(bashCommand);
@@ -3164,7 +3378,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3164
3378
  const matchedRule = resolvePinned(matches);
3165
3379
  if (matchedRule) {
3166
3380
  if (matchedRule.verdict === "allow")
3167
- return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
3381
+ return pendingAstReview ?? { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
3168
3382
  return {
3169
3383
  decision: matchedRule.verdict,
3170
3384
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
@@ -3191,6 +3405,7 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
3191
3405
  allTokens = analyzed.allTokens;
3192
3406
  pathTokens = analyzed.paths;
3193
3407
  const candidates = [];
3408
+ if (pendingAstReview) candidates.push(pendingAstReview);
3194
3409
  const evalVerdict = detectDangerousShellExec(shellCommand);
3195
3410
  if (evalVerdict === "block") {
3196
3411
  return {
@@ -4297,6 +4512,12 @@ function classifyRuleSeverity(name, verdict) {
4297
4512
  "read-ssh",
4298
4513
  "read-gcp",
4299
4514
  "read-cred",
4515
+ // Stage 4 (2026-09-11): a copy of a credential file scores like a READ of it --
4516
+ // read-ssh/aws/cred are critical, so copy-ssh/aws/cred are; read-env is high
4517
+ // (below), so copy-env joins the high list, not this one (/code-review).
4518
+ "copy-ssh",
4519
+ "copy-aws",
4520
+ "copy-cred",
4300
4521
  "delete-repo",
4301
4522
  "helm-uninstall",
4302
4523
  "drop-table",
@@ -4309,6 +4530,7 @@ function classifyRuleSeverity(name, verdict) {
4309
4530
  ];
4310
4531
  if (criticalPatterns.some((p) => n.includes(p))) return "critical";
4311
4532
  const highPatterns = [
4533
+ "copy-env",
4312
4534
  "force-push",
4313
4535
  "force_push",
4314
4536
  "git-destructive",
@@ -4326,6 +4548,11 @@ function narrativeRuleLabel(name) {
4326
4548
  const map = {
4327
4549
  "read-aws": "AWS credentials read",
4328
4550
  "read-ssh": "SSH private key read",
4551
+ // Stage 4 copy twins, so `scan --narrative` prints a label, not a raw slug.
4552
+ "copy-ssh": "SSH private key copied out",
4553
+ "copy-aws": "AWS credentials copied out",
4554
+ "copy-env": ".env file copied out",
4555
+ "copy-cred": "credential file copied out",
4329
4556
  "read-gcp": "GCP credentials read",
4330
4557
  "read-cred": "credential file read",
4331
4558
  "delete-repo": "GitHub repository deletion",
@@ -4723,8 +4950,8 @@ function matchCanaryArgs(args, values) {
4723
4950
 
4724
4951
  // src/scan/canonical.ts
4725
4952
  var LONG_OUTPUT_THRESHOLD_BYTES = 100 * 1024;
4726
- var CANONICAL_EXTRACTOR_VERSION = "canonical-v13";
4727
- var CANONICAL_EXTRACTOR_HASH = "4622a2f41696af04";
4953
+ var CANONICAL_EXTRACTOR_VERSION = "canonical-v14";
4954
+ var CANONICAL_EXTRACTOR_HASH = "8b5729fe236a195b";
4728
4955
  var DEDUPE_PREVIEW_LEN = 120;
4729
4956
  function extractCanonicalFindings(call, ctx) {
4730
4957
  const out = [];
@@ -5100,6 +5327,7 @@ export {
5100
5327
  CANONICAL_EXTRACTOR_HASH,
5101
5328
  CANONICAL_EXTRACTOR_VERSION,
5102
5329
  COMMAND_WRAPPERS,
5330
+ COPY_VERBS,
5103
5331
  COST_PER_LOOP_ITER_USD,
5104
5332
  DEFAULT_EGRESS_ALLOWLIST,
5105
5333
  DESTINATION_ARGS,
@@ -5169,9 +5397,11 @@ export {
5169
5397
  normalizeIpLiteral,
5170
5398
  parseAllSshHostsFromCommand,
5171
5399
  parseDestHost,
5400
+ positionedArgs,
5172
5401
  previewArgs,
5173
5402
  redactText,
5174
5403
  resolvePinned,
5404
+ sampleCopyCommand,
5175
5405
  scanArgs,
5176
5406
  scanInjection,
5177
5407
  scanText,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/policy-engine",
3
- "version": "2.13.1",
3
+ "version": "2.14.0",
4
4
  "description": "Shared policy evaluation engine for node9 — DLP, smart rules, AST shell parsing, shields, loop detection. Pure functions, no I/O. Used by both node9-proxy and the node9 SaaS firewall.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://node9.ai",