@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 +51 -3
- package/dist/index.d.ts +51 -3
- package/dist/index.js +256 -23
- package/dist/index.mjs +253 -23
- package/package.json +1 -1
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-
|
|
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 = "
|
|
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-
|
|
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 = "
|
|
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
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1888
|
-
|
|
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)
|
|
1895
|
-
|
|
1896
|
-
|
|
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
|
|
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
|
-
|
|
2342
|
-
|
|
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
|
|
2346
|
-
|
|
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
|
-
|
|
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-
|
|
4857
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
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
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
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)
|
|
1765
|
-
|
|
1766
|
-
|
|
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
|
|
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
|
-
|
|
2212
|
-
|
|
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
|
|
2216
|
-
|
|
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
|
-
|
|
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-
|
|
4727
|
-
var CANONICAL_EXTRACTOR_HASH = "
|
|
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.
|
|
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",
|