renma 0.2.0 → 0.4.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.
@@ -1,403 +1,1429 @@
1
- const constraints = [
2
- "Do not remove legitimate setup steps solely because this finding exists.",
3
- "Preserve required platform-specific setup instructions.",
4
- "Prefer pinning, scoping, confirmation, and recovery guidance over deleting operational detail.",
5
- ];
6
- const verificationSteps = [
7
- "Review the reported command or instruction.",
8
- "Add version pinning, checksum verification, confirmation, scoping, or recovery steps as appropriate.",
9
- "Run `renma scan` again.",
10
- ];
11
- const remoteScriptMetadata = {
12
- id: "SEC-UNPINNED-REMOTE-SCRIPT",
13
- title: "Remote script execution without pinning or inspection",
14
- whyItMatters: "Agent-facing instructions that pipe remote code directly into a shell can cause unreviewed code execution.",
15
- remediation: "Download the script first, inspect it, pin the source/version/checksum, and run it only after human approval.",
16
- llmHint: "Replace one-line remote shell execution with download, checksum/version pinning, inspection, and explicit human approval steps.",
17
- };
18
- const unpinnedDependencyMetadata = {
19
- id: "SEC-UNPINNED-DEPENDENCY-INSTALL",
20
- title: "Dependency install without a version pin",
21
- whyItMatters: "Agent-facing install instructions that resolve the latest package can change behavior between runs and pull unreviewed code.",
22
- remediation: "Pin package versions or document why the latest version is intentionally required.",
23
- llmHint: "Prefer explicit package versions, image tags, or a written reason why latest is required.",
24
- };
25
- const privilegedCommandMetadata = {
26
- id: "SEC-PRIVILEGED-COMMAND-WITHOUT-GUARD",
27
- title: "Privileged command without nearby guardrails",
28
- whyItMatters: "Privileged commands in reusable instructions can alter system state broadly when copied by an agent without confirmation or rollback guidance.",
29
- remediation: "Add explicit scope, confirmation, and rollback/verification instructions before privileged commands.",
30
- llmHint: "Keep the setup step if it is required, but add confirmation, path scope, backup or rollback, and verification language nearby.",
31
- };
32
- const predictableTempMetadata = {
33
- id: "SEC-PREDICTABLE-TEMP-PATH",
34
- title: "Predictable temporary path in operational instructions",
35
- whyItMatters: "Predictable temporary paths can expose sensitive files or collide with existing files when reused by agents or scripts.",
36
- remediation: "Use `mktemp` or a workspace-scoped temporary directory, and avoid predictable paths for sensitive data.",
37
- llmHint: "Use mktemp or a repository-scoped temporary path, especially for tokens, credentials, signing material, or auth files.",
38
- };
39
- const credentialArgMetadata = {
40
- id: "SEC-CREDENTIAL-IN-COMMAND-ARG",
41
- title: "Credential-like value embedded in a command argument",
42
- whyItMatters: "Credentials in command arguments or reusable headers can leak through shell history, process listings, logs, or copied instructions.",
43
- remediation: "Use environment variables, secret managers, or interactive prompts. Avoid putting credentials directly in command arguments or reusable instructions.",
44
- llmHint: "Move literal secrets out of command arguments and into environment variables, secret managers, or interactive prompts.",
1
+ const RULES = {
2
+ missingPolicyMetadata: {
3
+ id: "SEC-MISSING-POLICY-METADATA",
4
+ category: "safety",
5
+ title: "Security-sensitive instructions are missing policy metadata",
6
+ whyItMatters: "LLM-facing security policy metadata gives humans and agents a deterministic contract for network, upload, and secret-handling instructions.",
7
+ remediation: "Add explicit security policy metadata such as network_allowed, external_upload_allowed, secrets_allowed, and any approved network destinations.",
8
+ constraints: [
9
+ "Keep the policy deterministic and local to the artifact.",
10
+ "Do not infer approval from prose alone.",
11
+ "Preserve existing repository governance metadata.",
12
+ ],
13
+ verificationSteps: [
14
+ "Run renma scan.",
15
+ "Confirm the artifact declares the relevant policy fields.",
16
+ "Review the security-sensitive instruction against the declared policy.",
17
+ ],
18
+ llmHint: "Add small frontmatter policy fields that describe whether network access, external uploads, and secret material are allowed for this artifact.",
19
+ confidence: "medium",
20
+ },
21
+ policyContradiction: {
22
+ id: "SEC-POLICY-CONTRADICTION",
23
+ category: "safety",
24
+ title: "Security policy fields contradict each other",
25
+ whyItMatters: "Contradictory policy metadata makes deterministic review ambiguous and can cause an agent to follow the less restrictive interpretation.",
26
+ remediation: "Make the policy internally consistent, or split the artifact so each instruction set has one clear policy.",
27
+ constraints: [
28
+ "Do not weaken restrictions without human review.",
29
+ "Keep network and upload allowances explicit.",
30
+ ],
31
+ verificationSteps: [
32
+ "Run renma scan.",
33
+ "Confirm contradictory policy fields no longer appear together.",
34
+ ],
35
+ llmHint: "Resolve the policy by choosing the stricter allowed behavior or by separating instructions into different assets with explicit metadata.",
36
+ confidence: "high",
37
+ },
38
+ bodyPolicyContradiction: {
39
+ id: "SEC-BODY-POLICY-CONTRADICTION",
40
+ category: "safety",
41
+ title: "Security policy metadata contradicts the instruction body",
42
+ whyItMatters: "Conflicting body text and policy metadata make deterministic review ambiguous and can cause an agent to follow the less restrictive instruction.",
43
+ remediation: "Make the body instructions and security metadata agree, or split them into separate artifacts with explicit policy fields.",
44
+ constraints: [
45
+ "deterministic",
46
+ "compares policy metadata with simple body denials",
47
+ "does not classify intent",
48
+ ],
49
+ verificationSteps: [
50
+ "Run renma scan and confirm policy fields match the body instructions.",
51
+ ],
52
+ llmHint: "Resolve body and metadata conflicts by choosing the stricter behavior or separating conflicting instructions into different assets.",
53
+ confidence: "high",
54
+ },
55
+ policyProfileNotFound: {
56
+ id: "SEC-POLICY-PROFILE-NOT-FOUND",
57
+ category: "safety",
58
+ title: "Referenced security profile is not configured",
59
+ whyItMatters: "A missing security profile makes artifact policy resolution ambiguous and can hide intended network, upload, and secret-handling constraints.",
60
+ remediation: "Add the named profile under security.profiles or update the artifact to reference an existing profile.",
61
+ constraints: [
62
+ "Do not silently ignore profile references.",
63
+ "Keep profile names deterministic and repo-local.",
64
+ ],
65
+ verificationSteps: [
66
+ "Run renma scan.",
67
+ "Confirm the referenced security profile exists in configuration.",
68
+ ],
69
+ llmHint: "Use a configured security_profile value, or add the missing profile under security.profiles with explicit policy fields.",
70
+ confidence: "high",
71
+ },
72
+ policyProfileCycle: {
73
+ id: "SEC-POLICY-PROFILE-CYCLE",
74
+ category: "safety",
75
+ title: "Security profile inheritance cycle detected",
76
+ whyItMatters: "Cyclic profile inheritance prevents deterministic policy resolution and can make agents miss stricter inherited restrictions.",
77
+ remediation: "Break the profile inheritance cycle so each profile resolves through an acyclic chain.",
78
+ constraints: [
79
+ "Do not resolve cycles by choosing the least restrictive profile.",
80
+ "Keep inherited policy chains short and explicit.",
81
+ ],
82
+ verificationSteps: [
83
+ "Run renma scan.",
84
+ "Confirm profile inheritance resolves without revisiting the same profile.",
85
+ ],
86
+ llmHint: "Remove or rewrite the cyclic profile reference so the selected security profile has a deterministic parent chain.",
87
+ confidence: "high",
88
+ },
89
+ policyOverrideContradiction: {
90
+ id: "SEC-POLICY-OVERRIDE-CONTRADICTION",
91
+ category: "safety",
92
+ title: "Security profile conflicts with stricter artifact policy",
93
+ whyItMatters: "Profile or repository allowances cannot override artifact-local explicit denials without making the policy contract ambiguous.",
94
+ remediation: "Keep the artifact-local denial and remove conflicting inherited allowances, or split the artifact into separately governed instructions.",
95
+ constraints: [
96
+ "Artifact-local explicit denials remain strict.",
97
+ "Do not weaken local restrictions through profile inheritance.",
98
+ ],
99
+ verificationSteps: [
100
+ "Run renma scan.",
101
+ "Confirm inherited policy does not contradict explicit artifact denials.",
102
+ ],
103
+ llmHint: "Treat explicit false policy fields in the artifact as authoritative and adjust the referenced profile or repo-level security config.",
104
+ confidence: "high",
105
+ },
106
+ forbiddenInputInstruction: {
107
+ id: "SEC-FORBIDDEN-INPUT-INSTRUCTION",
108
+ category: "safety",
109
+ title: "Instruction requests data forbidden by security profile",
110
+ whyItMatters: "Profile-level forbidden inputs define data classes that must not be collected, copied, uploaded, or summarized by LLM-facing instructions.",
111
+ remediation: "Remove the forbidden input request or choose a profile whose allowed data contract covers the instruction.",
112
+ constraints: [
113
+ "Do not reinterpret forbidden inputs as allowed data.",
114
+ "Keep profile data-class restrictions explicit.",
115
+ ],
116
+ verificationSteps: [
117
+ "Run renma scan.",
118
+ "Confirm the artifact no longer instructs agents to handle forbidden inputs.",
119
+ ],
120
+ llmHint: "Rewrite the instruction so it avoids profile-forbidden inputs such as secrets, credentials, private keys, or customer data.",
121
+ confidence: "high",
122
+ },
123
+ instructionViolatesPolicy: {
124
+ id: "SEC-INSTRUCTION-VIOLATES-POLICY",
125
+ category: "safety",
126
+ title: "Instruction appears to violate declared security policy",
127
+ whyItMatters: "A deterministic policy denial should override LLM-facing operational instructions that ask for network, upload, or secret handling.",
128
+ remediation: "Remove or rewrite the violating instruction, or update the policy only after an explicit human security review.",
129
+ constraints: [
130
+ "Do not silently relax network, upload, or secret restrictions.",
131
+ "Preserve the artifact's intended workflow where it can be made policy-compliant.",
132
+ ],
133
+ verificationSteps: [
134
+ "Run renma scan.",
135
+ "Confirm no instruction conflicts with the declared policy.",
136
+ ],
137
+ llmHint: "Find the instruction that asks for denied behavior and rewrite it to stay within the artifact's declared security policy.",
138
+ confidence: "high",
139
+ },
140
+ missingHumanApprovalGuard: {
141
+ id: "SEC-MISSING-HUMAN-APPROVAL-GUARD",
142
+ category: "safety",
143
+ title: "Sensitive external action lacks a human approval guard",
144
+ whyItMatters: "Instructions that send data externally should clearly require human confirmation before an agent performs the action.",
145
+ remediation: "Add an explicit approval, confirmation, or review guard before external network or upload actions.",
146
+ constraints: [
147
+ "Do not replace approval with vague cautionary language.",
148
+ "Keep the guard close to the sensitive instruction.",
149
+ ],
150
+ verificationSteps: [
151
+ "Run renma scan.",
152
+ "Confirm the sensitive action is guarded by nearby approval language.",
153
+ ],
154
+ llmHint: "Insert an explicit human approval requirement next to upload, POST, cloud sync, or external sharing instructions.",
155
+ confidence: "medium",
156
+ },
157
+ sensitiveFileReference: {
158
+ id: "SEC-SENSITIVE-FILE-REFERENCE",
159
+ category: "safety",
160
+ title: "Instruction references sensitive file material",
161
+ whyItMatters: "Private keys, signing material, credential stores, and environment files need deliberate handling before they are read, copied, or attached to agent context.",
162
+ remediation: "Remove unnecessary sensitive file references or add explicit handling rules that prevent disclosure.",
163
+ constraints: [
164
+ "Do not expose file contents in diagnostics.",
165
+ "Keep allowlisted sample paths separate from real secret material.",
166
+ ],
167
+ verificationSteps: [
168
+ "Run renma scan.",
169
+ "Confirm sensitive file references are removed, mocked, or protected by policy.",
170
+ ],
171
+ llmHint: "Inspect this reference and either replace it with a safe placeholder or add explicit no-disclosure handling instructions.",
172
+ confidence: "high",
173
+ },
174
+ secretMaterialInstruction: {
175
+ id: "SEC-SECRET-MATERIAL-INSTRUCTION",
176
+ category: "safety",
177
+ title: "Instruction may expose secret material",
178
+ whyItMatters: "LLM-facing instructions that copy, print, paste, upload, or summarize secrets can leak credentials even when no literal secret value appears in the repository.",
179
+ remediation: "Rewrite the instruction to avoid exposing secret material and require redaction or human review when sensitive files are involved.",
180
+ constraints: [
181
+ "Do not include secret values in the repair.",
182
+ "Prefer safe placeholders and redaction guidance.",
183
+ ],
184
+ verificationSteps: [
185
+ "Run renma scan.",
186
+ "Confirm secret material is not requested for printing, copying, uploading, or context inclusion.",
187
+ ],
188
+ llmHint: "Rewrite this instruction so secret-bearing files are never copied into prompts, logs, uploads, or diagnostics.",
189
+ confidence: "high",
190
+ },
191
+ externalUploadInstruction: {
192
+ id: "SEC-EXTERNAL-UPLOAD-INSTRUCTION",
193
+ category: "safety",
194
+ title: "Instruction sends repository data to an external destination",
195
+ whyItMatters: "External uploads can disclose proprietary code, logs, credentials, customer data, or unreleased operational details.",
196
+ remediation: "Require explicit approval and destination review before uploading or sharing repository data externally.",
197
+ constraints: [
198
+ "Do not assume cloud or pastebin destinations are safe.",
199
+ "Keep approved destinations explicit in policy metadata.",
200
+ ],
201
+ verificationSteps: [
202
+ "Run renma scan.",
203
+ "Confirm uploads are either removed or guarded by explicit policy and approval.",
204
+ ],
205
+ llmHint: "Add a human approval gate and approved destination metadata, or replace the upload with a local-only workflow.",
206
+ confidence: "high",
207
+ },
208
+ unapprovedNetworkDestination: {
209
+ id: "SEC-UNAPPROVED-NETWORK-DESTINATION",
210
+ category: "safety",
211
+ title: "Instruction references an unapproved network destination",
212
+ whyItMatters: "Agents need deterministic destination allowlists when instructions mention external hosts, APIs, or storage services.",
213
+ remediation: "Add the destination to approved_network_destinations after review, or remove the external network instruction.",
214
+ constraints: [
215
+ "Do not use fuzzy destination matching.",
216
+ "Keep hostnames or URL prefixes explicit.",
217
+ ],
218
+ verificationSteps: [
219
+ "Run renma scan.",
220
+ "Confirm every external destination is approved or removed.",
221
+ ],
222
+ llmHint: "Compare the referenced URL or host to approved_network_destinations and either approve it explicitly or remove the instruction.",
223
+ confidence: "high",
224
+ },
225
+ unapprovedUploadDestination: {
226
+ id: "SEC-UNAPPROVED-UPLOAD-DESTINATION",
227
+ category: "safety",
228
+ title: "Instruction references an unapproved upload destination",
229
+ whyItMatters: "Upload destinations need a stricter allowlist because they can receive repository data, logs, credentials, or private context.",
230
+ remediation: "Add the destination to security.approvedUploadDomains after review, or remove the upload instruction.",
231
+ constraints: [
232
+ "Do not treat general network approval as upload approval.",
233
+ "Keep upload destinations explicit and deterministic.",
234
+ ],
235
+ verificationSteps: [
236
+ "Run renma scan.",
237
+ "Confirm every upload destination is approved or removed.",
238
+ ],
239
+ llmHint: "Compare the referenced upload URL or host to security.approvedUploadDomains and either approve it explicitly or remove the instruction.",
240
+ confidence: "high",
241
+ },
242
+ bulkDataSharingInstruction: {
243
+ id: "SEC-BULK-DATA-SHARING-INSTRUCTION",
244
+ category: "safety",
245
+ title: "Instruction asks to share broad repository or context data",
246
+ whyItMatters: "Bulk sharing instructions can leak more information than the task needs and are risky when followed by an LLM agent.",
247
+ remediation: "Narrow the instruction to the minimum files, snippets, or sanitized summary needed for review.",
248
+ constraints: [
249
+ "Do not ask an agent to paste entire repositories, logs, or context bundles.",
250
+ "Prefer scoped evidence snippets over bulk data transfer.",
251
+ ],
252
+ verificationSteps: [
253
+ "Run renma scan.",
254
+ "Confirm sharing instructions name a bounded, minimal data set.",
255
+ ],
256
+ llmHint: "Replace broad sharing language with scoped file paths, limited snippets, and redaction requirements.",
257
+ confidence: "medium",
258
+ },
259
+ cloudUploadInstruction: {
260
+ id: "SEC-CLOUD-UPLOAD-INSTRUCTION",
261
+ category: "safety",
262
+ title: "Instruction uploads data to cloud storage or cloud services",
263
+ whyItMatters: "Cloud upload instructions often move repository data outside local review boundaries and should be explicitly approved.",
264
+ remediation: "Replace cloud upload with a local artifact, or require explicit approval and approved destination metadata.",
265
+ constraints: [
266
+ "Do not treat generic cloud storage as approved by default.",
267
+ "Keep external upload policy explicit.",
268
+ ],
269
+ verificationSteps: [
270
+ "Run renma scan.",
271
+ "Confirm cloud uploads are removed, approved, or guarded.",
272
+ ],
273
+ llmHint: "Turn the cloud upload into a local-only output, or add policy metadata and a human approval guard.",
274
+ confidence: "medium",
275
+ },
276
+ overbroadContextInstruction: {
277
+ id: "SEC-OVERBROAD-CONTEXT-INSTRUCTION",
278
+ category: "safety",
279
+ title: "Instruction requests overbroad context collection",
280
+ whyItMatters: "Overbroad context collection encourages agents to ingest unnecessary files, logs, or private data before a task requires it.",
281
+ remediation: "Scope the instruction to relevant files, folders, or evidence snippets and exclude secret-bearing material.",
282
+ constraints: [
283
+ "Do not introduce runtime context selection.",
284
+ "Keep guidance deterministic and repository-local.",
285
+ ],
286
+ verificationSteps: [
287
+ "Run renma scan.",
288
+ "Confirm context instructions are scoped and exclude sensitive material.",
289
+ ],
290
+ llmHint: "Replace broad context collection with bounded paths, task-relevant snippets, and explicit exclusions for secrets.",
291
+ confidence: "medium",
292
+ },
293
+ noRedactionInstruction: {
294
+ id: "SEC-NO-REDACTION-INSTRUCTION",
295
+ category: "safety",
296
+ title: "Instruction discourages redaction of sensitive data",
297
+ whyItMatters: "Telling agents not to redact data can cause credentials, customer data, or internal details to appear in prompts, logs, or uploads.",
298
+ remediation: "Remove the no-redaction instruction and require redaction for secrets, credentials, tokens, personal data, and proprietary values.",
299
+ constraints: [
300
+ "Do not weaken redaction requirements.",
301
+ "Keep examples synthetic where possible.",
302
+ ],
303
+ verificationSteps: [
304
+ "Run renma scan.",
305
+ "Confirm instructions require redaction where sensitive data may appear.",
306
+ ],
307
+ llmHint: "Replace no-redaction wording with explicit redaction requirements for secrets and sensitive data.",
308
+ confidence: "high",
309
+ },
310
+ unpinnedRemoteScript: {
311
+ id: "SEC-UNPINNED-REMOTE-SCRIPT",
312
+ category: "safety",
313
+ title: "Remote install script is not pinned",
314
+ whyItMatters: "Piping a mutable remote script into a shell gives the destination server control over code executed by the agent or developer.",
315
+ remediation: "Replace the pipe-to-shell command with a pinned release artifact, checksum verification, or manually reviewed local script.",
316
+ constraints: [
317
+ "Do not execute the remote script during remediation.",
318
+ "Keep install guidance reproducible.",
319
+ ],
320
+ verificationSteps: [
321
+ "Run renma scan.",
322
+ "Confirm remote script execution is removed or pinned with verification.",
323
+ ],
324
+ llmHint: "Rewrite the install instruction to download a pinned artifact and verify it before execution.",
325
+ confidence: "high",
326
+ },
327
+ unpinnedDependencyInstall: {
328
+ id: "SEC-UNPINNED-DEPENDENCY-INSTALL",
329
+ category: "safety",
330
+ title: "Dependency install is not pinned",
331
+ whyItMatters: "Unpinned dependencies make agent setup non-reproducible and can unexpectedly pull compromised or breaking packages.",
332
+ remediation: "Pin package, image, and formula versions or refer to the repository lockfile.",
333
+ constraints: [
334
+ "Do not pick arbitrary versions without checking the repository's intended support matrix.",
335
+ "Preserve existing package manager conventions.",
336
+ ],
337
+ verificationSteps: [
338
+ "Run renma scan.",
339
+ "Confirm dependency install instructions are pinned or lockfile-based.",
340
+ ],
341
+ llmHint: "Pin packages, images, or formulas in setup instructions, or route through the repository's lockfile command.",
342
+ confidence: "medium",
343
+ },
344
+ privilegedCommandWithoutGuard: {
345
+ id: "SEC-PRIVILEGED-COMMAND-WITHOUT-GUARD",
346
+ category: "safety",
347
+ title: "Privileged command lacks a review guard",
348
+ whyItMatters: "Privileged commands can modify the host, containers, system package state, or file ownership outside the repository.",
349
+ remediation: "Add a human approval or review guard before privileged commands, or replace them with least-privilege alternatives.",
350
+ constraints: [
351
+ "Do not normalize privileged commands as routine setup.",
352
+ "Keep the guard close to the command.",
353
+ ],
354
+ verificationSteps: [
355
+ "Run renma scan.",
356
+ "Confirm privileged commands require approval or have been removed.",
357
+ ],
358
+ llmHint: "Add an explicit approval requirement before sudo, chmod/chown, docker privileged operations, or system writes.",
359
+ confidence: "medium",
360
+ },
361
+ dangerousToolInstruction: {
362
+ id: "SEC-DANGEROUS-TOOL-INSTRUCTION",
363
+ category: "safety",
364
+ title: "Instruction uses a disallowed tool or command",
365
+ whyItMatters: "Repository policy can ban tools that exfiltrate data, open raw sockets, or publish content outside reviewed workflows.",
366
+ remediation: "Remove the disallowed command or replace it with an approved, auditable workflow.",
367
+ constraints: [
368
+ "Do not bypass the configured disallowed command list.",
369
+ "Keep any replacement workflow deterministic and reviewable.",
370
+ ],
371
+ verificationSteps: [
372
+ "Run renma scan.",
373
+ "Confirm disallowed command instructions have been removed or rewritten.",
374
+ ],
375
+ llmHint: "Check security.disallowedCommands and remove instructions that invoke those commands or services.",
376
+ confidence: "high",
377
+ },
378
+ credentialInCommandArg: {
379
+ id: "SEC-CREDENTIAL-IN-COMMAND-ARG",
380
+ category: "safety",
381
+ title: "Command includes credential material in arguments",
382
+ whyItMatters: "Credentials in command arguments can leak through shell history, process lists, logs, diagnostics, or copied instructions.",
383
+ remediation: "Move credentials to approved secret storage, environment injection, or an interactive prompt that is not logged.",
384
+ constraints: [
385
+ "Do not preserve literal credential examples.",
386
+ "Use placeholders only when examples are necessary.",
387
+ ],
388
+ verificationSteps: [
389
+ "Run renma scan.",
390
+ "Confirm command examples do not include token, password, key, or certificate values.",
391
+ ],
392
+ llmHint: "Replace literal credential command arguments with safe placeholders and approved secret handling guidance.",
393
+ confidence: "high",
394
+ },
395
+ predictableTempPath: {
396
+ id: "SEC-PREDICTABLE-TEMP-PATH",
397
+ category: "safety",
398
+ title: "Instruction uses predictable temporary path for sensitive material",
399
+ whyItMatters: "Predictable temporary file paths can expose credentials, profiles, logs, or certificates to accidental reuse or disclosure.",
400
+ remediation: "Use a securely created temporary directory or repository-local ignored path with explicit cleanup.",
401
+ constraints: [
402
+ "Do not put sensitive material in shared /tmp paths.",
403
+ "Keep cleanup instructions explicit.",
404
+ ],
405
+ verificationSteps: [
406
+ "Run renma scan.",
407
+ "Confirm sensitive temporary paths are randomized, scoped, and cleaned up.",
408
+ ],
409
+ llmHint: "Replace predictable /tmp paths for profiles, credentials, certs, logs, or tokens with secure temporary directory handling.",
410
+ confidence: "medium",
411
+ },
45
412
  };
46
- const guardWords = [
47
- "confirm",
48
- "ask the user",
49
- "approval",
50
- "dry run",
51
- "backup",
52
- "restore",
53
- "rollback",
54
- "revert",
55
- "scope",
56
- "only this path",
57
- "verify",
58
- ];
59
- const sensitiveTempWords = [
60
- "token",
61
- "secret",
62
- "password",
63
- "credential",
64
- "key",
65
- "cert",
66
- "certificate",
67
- "profile",
68
- "provisioning",
69
- "signing",
70
- "auth",
413
+ const BOOLEAN_POLICY_FIELDS = new Map([
414
+ ["network_allowed", "networkAllowed"],
415
+ ["networkAllowed", "networkAllowed"],
416
+ ["external_upload_allowed", "externalUploadAllowed"],
417
+ ["externalUploadAllowed", "externalUploadAllowed"],
418
+ ["secrets_allowed", "secretsAllowed"],
419
+ ["secretsAllowed", "secretsAllowed"],
420
+ ["human_approval_required", "humanApprovalRequired"],
421
+ ["humanApprovalRequired", "humanApprovalRequired"],
422
+ ["requires_human_approval", "humanApprovalRequired"],
423
+ ["requiresHumanApproval", "humanApprovalRequired"],
424
+ ]);
425
+ const DESTINATION_POLICY_FIELDS = new Set([
426
+ "approved_network_destinations",
427
+ "approvedNetworkDestinations",
428
+ "allowed_network_destinations",
429
+ "allowedNetworkDestinations",
430
+ ]);
431
+ const UPLOAD_DESTINATION_POLICY_FIELDS = new Set([
432
+ "approved_upload_destinations",
433
+ "approvedUploadDestinations",
434
+ "approved_upload_domains",
435
+ "approvedUploadDomains",
436
+ ]);
437
+ const ALLOWED_DATA_POLICY_FIELDS = new Set(["allowed_data", "allowedData"]);
438
+ const FORBIDDEN_INPUT_POLICY_FIELDS = new Set([
439
+ "forbidden_inputs",
440
+ "forbiddenInputs",
441
+ ]);
442
+ const SECURITY_PROFILE_POLICY_FIELDS = new Set([
443
+ "security_profile",
444
+ "securityProfile",
445
+ ]);
446
+ const FUTURE_BLOCK_LIST_POLICY_FIELDS = new Set([
447
+ "allowed_tools",
448
+ "allowedTools",
449
+ "allowed_clients",
450
+ "allowedClients",
451
+ ]);
452
+ const FORBIDDEN_INPUT_ACTION_PATTERN = /\b(copy|print|cat|echo|paste|upload|send|share|attach|include|dump|export|log|summari[sz]e|read|collect|provide|load|use)\b/i;
453
+ const SAFE_FORBIDDEN_INPUT_PATTERN = /\b(do\s+not|don't|never|avoid|exclude|without|redact|remove|omit|strip|skip)\b.{0,80}\b(secret|secrets|credential|credentials|token|password|private key|private keys|\.env|env files?|customer data)\b/i;
454
+ const BODY_NETWORK_DISALLOWED_RE = /\b(no|without|avoid|exclude|disallow|forbid|forbidden|block|do\s+not|don't|never)\b.{0,80}\b(network|internet|external|remote|http|https|api|webhook|download|fetch|curl|wget)\b/i;
455
+ const BODY_UPLOAD_DISALLOWED_RE = /\b(no|without|avoid|exclude|disallow|forbid|forbidden|block|do\s+not|don't|never)\b.{0,80}\b(upload|send|post|share|attach|submit|sync|push|publish|external upload|third-party)\b/i;
456
+ const BODY_SECRET_DISALLOWED_RE = /\b(no|without|avoid|exclude|disallow|forbid|forbidden|block|do\s+not|don't|never)\b.{0,80}\b(secret|secrets|credential|credentials|token|password|private key|private keys|\.env|env files?|customer data)\b/i;
457
+ const NETWORK_ACTION_RE = /\b(curl|wget|http|https|api|webhook|post|get|upload|download|fetch|send|sync|push)\b|https?:\/\//i;
458
+ const NETWORK_DESTINATION_RE = /\b(?:https?:\/\/)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{1,62}(?::\d+)?(?:\/[^\s"'`<>{}[\]]*)?/gi;
459
+ const TRAILING_DESTINATION_PUNCTUATION_RE = /[),.;:!?]+$/;
460
+ const EXTERNAL_UPLOAD_RE = /\b(upload|send|post|share|attach|submit|sync|push|publish)\b.*\b(external|remote|third[- ]party|pastebin|gist|slack|discord|s3|gcs|cloud|storage|bucket|drive|dropbox|notion|jira|github)\b|\b(post|put)\b.*https?:\/\//i;
461
+ const CLOUD_UPLOAD_RE = /\b(upload|sync|copy|send|push|publish)\b.*\b(s3|gcs|cloud storage|bucket|drive|dropbox|box|onedrive|blob storage|azure storage|storage)\b/i;
462
+ const UPLOAD_DESTINATION_ACTION_RE = /\b(upload|send|post|put|share|attach|submit|sync|push|publish|copy)\b|--data(?:-binary)?\b|-X\s*(?:POST|PUT)\b/i;
463
+ const BULK_DATA_RE = /\b(entire|whole|all|full|complete|raw)\b.*\b(repo|repository|workspace|codebase|project|context|logs?|files?|history|dataset)\b|\b(paste|upload|send|share|attach)\b.*\b(everything|all files|full logs|full context|entire repo|whole repository)\b/i;
464
+ const UNDISCLOSED_DATA_RE = /\b(include|attach|paste|upload|send|share|dump|export|print|collect|provide)\b.*\b(all|entire|full|complete|raw)\b.*\b(environment variables|env vars|env|process\.env|secrets?|credentials?|tokens?)\b/i;
465
+ const OVERBROAD_CONTEXT_RE = /\b(load|read|include|attach|paste|ingest|collect|provide|send)\b.*\b(entire|whole|all|full|complete|raw)\b.*\b(repo|repository|workspace|codebase|context|logs?|files?)\b/i;
466
+ const NO_REDACTION_RE = /\b(do not|don't|without|no|never)\b.{0,30}\b(redact|redaction|sanitize|mask|obfuscate)\b|\b(redact|sanitize|mask|obfuscate)\b.{0,30}\b(disabled|false|off)\b/i;
467
+ const APPROVAL_RE = /\b(human|user|owner|maintainer|reviewer|security)\b.{0,40}\b(approve|approval|confirm|confirmation|review|consent|authorize|authorization)\b|\b(ask|require|obtain|wait for)\b.{0,40}\b(approval|confirmation|consent|authorization)\b/i;
468
+ const SECRET_WORD_RE = /\b(secret|secrets|credential|credentials|token|password|passwd|api key|apikey|private key|ssh key|signing key|certificate|cert|auth)\b/i;
469
+ const SECRET_ACTION_RE = /\b(copy|print|cat|echo|paste|upload|send|share|attach|include|dump|export|log|summari[sz]e|read)\b/i;
470
+ const SAFE_NEGATION_RE = /\b(not|never|avoid|exclude|without|redact|mock|fake|sample|placeholder|dummy)\b.{0,40}\b(secret|secrets|credential|credentials|token|password|private key)\b|\b(secret|secrets|credential|credentials|token|password|private key)\b.{0,40}\b(not|never|avoid|exclude|redact|mock|fake|sample|placeholder|dummy)\b/i;
471
+ const REMOTE_SCRIPT_RE = /\b(curl|wget)\b[^\n]*?(https?:\/\/[^\s|`'")]+)[^\n]*(\|\s*(sh|bash|zsh)|\b(sh|bash|zsh)\b)/i;
472
+ const PRIVILEGED_COMMAND_RE = /\b(sudo|chmod\s+(777|666|\+w|a\+w)|chown\b|docker\s+run\b[^\n]*(--privileged|-v\s+\/|--pid=host)|mount\b|launchctl\b|systemctl\b)\b/i;
473
+ const DESTRUCTIVE_COMMAND_RE = /\b(rm\s+-[^\n]*[rf][^\n]*|git\s+reset\s+--hard|git\s+clean\s+-[^\n]*[xdf][^\n]*|docker\s+(?:rm|rmi|system\s+prune|volume\s+rm)\b|kubectl\s+delete\b|drop\s+database|truncate\s+table)\b/i;
474
+ const CREDENTIAL_ARG_RE = /--?(token|password|passwd|secret|credential|api[-_]?key|key|cert|certificate|signing[-_]?key|auth)(=|\s+)(?!<|\$|\{|\[|REDACTED|redacted|xxx|XXX|placeholder|example)[^\s"'`]+/i;
475
+ const CREDENTIAL_ARG_ANY_RE = /--?(token|password|passwd|secret|credential|api[-_]?key|key|cert|certificate|signing[-_]?key|auth)(=|\s+)[^\s"'`]+/i;
476
+ const CREDENTIAL_HEADER_RE = /\bAuthorization:\s*Bearer\s+(?!<|\$|\{|\[|REDACTED|redacted|xxx|XXX|placeholder|example)[^\s"'`]+/i;
477
+ const PREDICTABLE_TEMP_RE = /\/tmp\/[A-Za-z0-9._/-]+/;
478
+ const PREDICTABLE_TEMP_GLOBAL_RE = /\/tmp\/[A-Za-z0-9._/-]+/g;
479
+ const SENSITIVE_FILE_PATTERNS = [
480
+ /\.env(?:\b|\.|$)/i,
481
+ /(^|[/\s])id_(rsa|dsa|ecdsa|ed25519)(?:\b|$)/i,
482
+ /(^|[/\s])\.?ssh\/(?:config|id_[A-Za-z0-9_-]+)/i,
483
+ /\.(p12|pfx|pem|key|p8|mobileprovision)(?:\b|$)/i,
484
+ /(^|[/\s])kubeconfig(?:\b|$)/i,
485
+ /(^|[/\s])\.kube\/config(?:\b|$)/i,
486
+ /(^|[/\s])\.aws\/credentials(?:\b|$)/i,
487
+ /(^|[/\s])credentials\.json(?:\b|$)/i,
488
+ /(^|[/\s])service-account(?:\b|\.json|$)/i,
489
+ /(^|[/\s])secrets?\.(json|ya?ml|toml|env)(?:\b|$)/i,
71
490
  ];
72
- const commandStarters = [
73
- "bash",
74
- "brew",
75
- "chmod",
76
- "chown",
77
- "curl",
78
- "docker",
79
- "npm",
80
- "pip",
81
- "pip3",
82
- "sh",
83
- "sudo",
84
- "wget",
85
- ];
86
- export function securityDiagnosticFindings(artifacts) {
87
- return artifacts
88
- .flatMap((artifact) => securityFindingsForArtifact(artifact))
89
- .sort(compareDiagnostics);
491
+ const CLOUD_DESTINATION_RE = /\b(s3:\/\/|gs:\/\/|az:\/\/|https?:\/\/(?:[^/\s]+\.)?(?:s3|storage|blob|drive|dropbox|box|onedrive|pastebin|gist|slack|discord)[^/\s]*\S*)/i;
492
+ export function securityDiagnosticFindings(artifacts, config = {}) {
493
+ return artifacts.flatMap((artifact) => securityFindingsForArtifact(artifact, config.security));
90
494
  }
91
- function securityFindingsForArtifact(artifact) {
495
+ function securityFindingsForArtifact(artifact, securityConfig) {
496
+ const parsedPolicy = parseSecurityPolicy(artifact.content);
497
+ const policy = applySecurityConfig(parsedPolicy, securityConfig);
498
+ const detections = securityPolicyResolutionDetections(parsedPolicy, policy, securityConfig, artifact.content);
92
499
  const lines = artifact.content.split(/\r?\n/);
93
- const findings = [];
500
+ const frontmatterEnd = lines[0]?.trim() === "---"
501
+ ? lines.findIndex((line, index) => index > 0 && line.trim() === "---")
502
+ : -1;
503
+ const scanStart = frontmatterEnd > 0 ? frontmatterEnd + 1 : 0;
94
504
  let inFence = false;
95
- for (let index = 0; index < lines.length; index += 1) {
505
+ let recentApprovalLine = 0;
506
+ if ((artifact.kind === "skill" || artifact.kind === "context") &&
507
+ effectiveAllowedDataClass(policy) === undefined &&
508
+ effectiveAllowedDataList(policy).length === 0) {
509
+ detections.push({
510
+ metadata: RULES.missingPolicyMetadata,
511
+ severity: "medium",
512
+ startLine: 1,
513
+ snippet: "missing allowed_data policy metadata",
514
+ });
515
+ }
516
+ detections.push(...bodyPolicyContradictionDetections(artifact.content, policy));
517
+ for (let index = scanStart; index < lines.length; index += 1) {
518
+ const lineNumber = index + 1;
96
519
  const line = lines[index] ?? "";
97
- const trimmed = line.trim();
98
- if (trimmed.startsWith("```")) {
520
+ if (/^\s*```/.test(line)) {
99
521
  inFence = !inFence;
522
+ }
523
+ const strippedComment = line.replace(/^\s*(#|\/\/)\s*/, "");
524
+ const shellComment = /^\s*(#|\/\/)/.test(line) &&
525
+ (inFence ||
526
+ isCommandLike(strippedComment) ||
527
+ CREDENTIAL_ARG_ANY_RE.test(strippedComment) ||
528
+ REMOTE_SCRIPT_RE.test(strippedComment) ||
529
+ PREDICTABLE_TEMP_RE.test(strippedComment));
530
+ if (shellComment) {
100
531
  continue;
101
532
  }
102
- if (isCommentOnlyShellLine(trimmed))
533
+ if (isPolicyLine(line)) {
103
534
  continue;
104
- const context = nearbyContext(lines, index);
105
- const operational = inFence || looksOperational(trimmed);
106
- const detections = [
107
- ...detectRemoteScript(trimmed, operational),
108
- ...detectUnpinnedDependency(trimmed, operational),
109
- ...detectPrivilegedCommand(trimmed, context, operational),
110
- ...detectPredictableTempPath(trimmed, context, operational),
111
- ...detectCredentialArgument(trimmed, operational),
112
- ];
113
- for (const detection of detections) {
114
- findings.push(toFinding(artifact.path, { ...detection, line: index + 1 }));
535
+ }
536
+ const hasApprovalGuard = hasNearbyApprovalLanguage(line) ||
537
+ (recentApprovalLine > 0 && lineNumber - recentApprovalLine <= 2);
538
+ const commandLine = !shellComment &&
539
+ (inFence ||
540
+ isCommandLike(line) ||
541
+ CREDENTIAL_ARG_ANY_RE.test(line) ||
542
+ CREDENTIAL_HEADER_RE.test(line));
543
+ detections.push(...policyDetections(line, lineNumber, policy, hasApprovalGuard));
544
+ detections.push(...disallowedCommandDetections(line, lineNumber, policy));
545
+ if (!commandLine || referencesSensitiveFile(line)) {
546
+ detections.push(...sensitiveDataDetections(line, lineNumber, policy));
547
+ }
548
+ if (!commandLine || policy.declared.size > 0) {
549
+ detections.push(...networkAndUploadDetections(line, lineNumber, policy));
550
+ }
551
+ detections.push(...contextScopeDetections(line, lineNumber));
552
+ detections.push(...predictableTempDetections(line, lineNumber));
553
+ if (commandLine) {
554
+ detections.push(...commandDetections(line, lineNumber, hasApprovalGuard));
555
+ }
556
+ if (hasNearbyApprovalLanguage(line)) {
557
+ recentApprovalLine = lineNumber;
115
558
  }
116
559
  }
117
- return collapsePredictableTempPathFindings(findings);
560
+ detections.push(...policyContradictions(policy));
561
+ return dedupeDetections(detections).map((detection) => findingFromDetection(artifact, detection));
118
562
  }
119
- function detectRemoteScript(line, operational) {
120
- if (!operational)
121
- return [];
122
- const directPipe = /\b(curl|wget)\b[^\n|]*(https?:\/\/\S+)[^\n|]*\|\s*(sudo\s+)?(ba)?sh\b/i.test(line);
123
- const processSubstitution = /\b(ba)?sh\s*<\(\s*(curl|wget)\b[^\n)]*https?:\/\/\S+/i.test(line);
124
- if (!directPipe && !processSubstitution)
563
+ function disallowedCommandDetections(line, lineNumber, policy) {
564
+ const matched = policy.disallowedCommands.find((command) => matchesDisallowedCommand(line, command));
565
+ if (matched === undefined)
125
566
  return [];
126
567
  return [
127
568
  {
128
- metadata: remoteScriptMetadata,
129
- severity: directPipe ? "high" : "medium",
130
- confidence: "high",
131
- line: 0,
569
+ metadata: RULES.dangerousToolInstruction,
570
+ severity: "high",
571
+ startLine: lineNumber,
132
572
  snippet: line,
573
+ dedupeKey: `${RULES.dangerousToolInstruction.id}:${matched.toLowerCase()}:${lineNumber}`,
133
574
  },
134
575
  ];
135
576
  }
136
- function detectUnpinnedDependency(line, operational) {
137
- if (!operational)
138
- return [];
577
+ function bodyPolicyContradictionDetections(content, policy) {
139
578
  const detections = [];
140
- const npm = line.match(/\bnpm\s+(?:install|i)\s+([^\n#]+)/i);
141
- if (npm) {
142
- const args = splitCommandArgs(npm[1] ?? "");
143
- const globalInstall = args.includes("-g") || args.includes("--global");
144
- const packages = args.filter((arg) => isPackageToken(arg) && arg !== "-g");
145
- const unpinned = packages.find((arg) => !isPinnedNpmPackage(arg));
146
- if (unpinned) {
579
+ const lines = content.split(/\r?\n/);
580
+ const frontmatterEnd = lines[0]?.trim() === "---"
581
+ ? lines.findIndex((line, index) => index > 0 && line.trim() === "---")
582
+ : -1;
583
+ const scanStart = frontmatterEnd > 0 ? frontmatterEnd + 1 : 0;
584
+ const emitted = new Set();
585
+ let inFence = false;
586
+ for (let index = scanStart; index < lines.length; index += 1) {
587
+ const line = lines[index] ?? "";
588
+ if (/^\s*```/.test(line)) {
589
+ inFence = !inFence;
590
+ continue;
591
+ }
592
+ if (inFence || isPolicyLine(line))
593
+ continue;
594
+ const lineNumber = index + 1;
595
+ const candidates = [
596
+ [
597
+ "network",
598
+ policy.networkAllowed === true && BODY_NETWORK_DISALLOWED_RE.test(line),
599
+ ],
600
+ [
601
+ "upload",
602
+ policy.externalUploadAllowed === true &&
603
+ BODY_UPLOAD_DISALLOWED_RE.test(line),
604
+ ],
605
+ [
606
+ "secrets",
607
+ policy.secretsAllowed === true && BODY_SECRET_DISALLOWED_RE.test(line),
608
+ ],
609
+ ];
610
+ for (const [kind, matched] of candidates) {
611
+ if (!matched || emitted.has(kind))
612
+ continue;
613
+ emitted.add(kind);
147
614
  detections.push({
148
- metadata: unpinnedDependencyMetadata,
149
- severity: globalInstall ? "medium" : "low",
150
- confidence: "medium",
151
- line: 0,
152
- snippet: line,
615
+ metadata: RULES.bodyPolicyContradiction,
616
+ severity: "high",
617
+ startLine: lineNumber,
618
+ snippet: line.trim(),
619
+ dedupeKey: `body-policy-contradiction:${kind}`,
153
620
  });
154
621
  }
155
622
  }
156
- const pip = line.match(/\bpip3?\s+install\s+([^\n#]+)/i);
157
- if (pip) {
158
- const packages = splitCommandArgs(pip[1] ?? "").filter(isPackageToken);
159
- const unpinned = packages.find((arg) => !/[<>=~!]=/.test(arg));
160
- if (unpinned) {
623
+ return detections;
624
+ }
625
+ function policyDetections(line, lineNumber, policy, hasApprovalGuard) {
626
+ const detections = [];
627
+ if (policy.networkAllowed === false && NETWORK_ACTION_RE.test(line)) {
628
+ detections.push({
629
+ metadata: RULES.instructionViolatesPolicy,
630
+ severity: "high",
631
+ startLine: lineNumber,
632
+ snippet: line,
633
+ });
634
+ }
635
+ if (policy.networkAllowed !== false &&
636
+ policy.approvedNetworkDestinations.length > 0) {
637
+ for (const destination of unapprovedNetworkDestinations(line, policy)) {
161
638
  detections.push({
162
- metadata: unpinnedDependencyMetadata,
163
- severity: "low",
164
- confidence: "medium",
165
- line: 0,
639
+ metadata: RULES.unapprovedNetworkDestination,
640
+ severity: "high",
641
+ startLine: lineNumber,
166
642
  snippet: line,
643
+ dedupeKey: destination.host + destination.path,
167
644
  });
168
645
  }
169
646
  }
170
- const brew = line.match(/\bbrew\s+install\s+([^\n#]+)/i);
171
- if (brew) {
172
- const packages = splitCommandArgs(brew[1] ?? "").filter(isPackageToken);
173
- if (packages.length > 0) {
647
+ if (policy.externalUploadAllowed === false && isUploadInstruction(line)) {
648
+ detections.push({
649
+ metadata: RULES.instructionViolatesPolicy,
650
+ severity: "high",
651
+ startLine: lineNumber,
652
+ snippet: line,
653
+ });
654
+ }
655
+ if (policy.externalUploadAllowed !== false &&
656
+ policy.approvedUploadDestinations.length > 0 &&
657
+ isUploadInstruction(line)) {
658
+ for (const destination of unapprovedDestinations(line, policy.approvedUploadDestinations)) {
174
659
  detections.push({
175
- metadata: unpinnedDependencyMetadata,
176
- severity: "low",
177
- confidence: "medium",
178
- line: 0,
660
+ metadata: RULES.unapprovedUploadDestination,
661
+ severity: "high",
662
+ startLine: lineNumber,
179
663
  snippet: line,
664
+ dedupeKey: destination.host + destination.path,
180
665
  });
181
666
  }
182
667
  }
183
- if (/\bdocker\s+(pull|run)\b[^\n#]*\S+:latest\b/i.test(line)) {
668
+ if (policy.secretsAllowed === false &&
669
+ SECRET_WORD_RE.test(line) &&
670
+ !SAFE_NEGATION_RE.test(line)) {
184
671
  detections.push({
185
- metadata: unpinnedDependencyMetadata,
186
- severity: "medium",
187
- confidence: "high",
188
- line: 0,
672
+ metadata: RULES.instructionViolatesPolicy,
673
+ severity: "high",
674
+ startLine: lineNumber,
189
675
  snippet: line,
190
676
  });
191
677
  }
192
- if (/\bcurl\b[^\n#]*https?:\/\/\S*\/latest(?:\/|\b|\S*)/i.test(line)) {
678
+ if (effectiveAllowedDataClass(policy)?.toLowerCase() === "disclosed" &&
679
+ UNDISCLOSED_DATA_RE.test(line)) {
193
680
  detections.push({
194
- metadata: unpinnedDependencyMetadata,
195
- severity: "low",
196
- confidence: "medium",
197
- line: 0,
681
+ metadata: RULES.instructionViolatesPolicy,
682
+ severity: "high",
683
+ startLine: lineNumber,
198
684
  snippet: line,
199
685
  });
200
686
  }
201
- return detections;
202
- }
203
- function detectPrivilegedCommand(line, context, operational) {
204
- if (!operational)
205
- return [];
206
- const hasPrivilegedCommand = /\bsudo\s+(npm|rm|chmod|chown|sh|bash|curl|wget)\b/i.test(line) ||
207
- /\bchmod\s+-R\s+777\b/i.test(line) ||
208
- /\bchown\s+-R\b/i.test(line);
209
- if (!hasPrivilegedCommand || hasGuardrail(context))
210
- return [];
211
- return [
212
- {
213
- metadata: privilegedCommandMetadata,
687
+ const needsApproval = (EXTERNAL_UPLOAD_RE.test(line) || CLOUD_UPLOAD_RE.test(line)) &&
688
+ policy.humanApprovalRequired === true &&
689
+ !hasApprovalGuard;
690
+ if (needsApproval) {
691
+ detections.push({
692
+ metadata: RULES.missingHumanApprovalGuard,
214
693
  severity: "medium",
215
- confidence: "medium",
216
- line: 0,
694
+ startLine: lineNumber,
217
695
  snippet: line,
218
- },
219
- ];
696
+ });
697
+ }
698
+ return detections;
220
699
  }
221
- function detectPredictableTempPath(line, context, operational) {
222
- if (!operational || !/\/tmp\/[A-Za-z0-9._/-]+/.test(line))
223
- return [];
224
- const sensitive = sensitiveTempWords.some((word) => new RegExp(`\\b${word}\\b`, "i").test(context));
225
- return [
226
- {
227
- metadata: predictableTempMetadata,
228
- severity: sensitive ? "medium" : "low",
229
- confidence: sensitive ? "high" : "medium",
230
- line: 0,
231
- snippet: line,
232
- },
233
- ];
700
+ function policyContradictions(policy) {
701
+ const detections = [];
702
+ if (policy.networkAllowed === false &&
703
+ policy.externalUploadAllowed === true) {
704
+ detections.push({
705
+ metadata: RULES.policyContradiction,
706
+ severity: "high",
707
+ startLine: policy.lineByField.get("externalUploadAllowed") ?? 1,
708
+ snippet: "external_upload_allowed is true while network_allowed is false",
709
+ });
710
+ }
711
+ if (policy.secretsAllowed === true && policy.externalUploadAllowed === true) {
712
+ detections.push({
713
+ metadata: RULES.policyContradiction,
714
+ severity: "high",
715
+ startLine: policy.lineByField.get("secretsAllowed") ?? 1,
716
+ snippet: "secrets_allowed and external_upload_allowed are both true",
717
+ });
718
+ }
719
+ return detections;
234
720
  }
235
- function detectCredentialArgument(line, operational) {
236
- if (!operational)
237
- return [];
721
+ function sensitiveDataDetections(line, lineNumber, policy) {
238
722
  const detections = [];
239
- const optionMatch = line.match(/\B--(?:password|token)\s+(?:"([^"]+)"|'([^']+)'|(\S+))/i);
240
- const optionValue = optionMatch?.[1] ?? optionMatch?.[2] ?? optionMatch?.[3];
241
- if (optionValue && !isSafePlaceholder(optionValue)) {
723
+ const sensitiveFile = referencesSensitiveFile(line);
724
+ if (sensitiveFile && !isSafeSensitiveHandlingInstruction(line)) {
242
725
  detections.push({
243
- metadata: credentialArgMetadata,
726
+ metadata: RULES.sensitiveFileReference,
244
727
  severity: "high",
245
- confidence: "high",
246
- line: 0,
728
+ startLine: lineNumber,
729
+ snippet: line,
730
+ });
731
+ }
732
+ const exposesSecret = SECRET_ACTION_RE.test(line) &&
733
+ (SECRET_WORD_RE.test(line) || sensitiveFile) &&
734
+ !isSafeSensitiveHandlingInstruction(line);
735
+ if (exposesSecret) {
736
+ detections.push({
737
+ metadata: RULES.secretMaterialInstruction,
738
+ severity: policy.secretsAllowed === false || policy.externalUploadAllowed === true
739
+ ? "critical"
740
+ : "high",
741
+ startLine: lineNumber,
247
742
  snippet: line,
248
743
  });
249
744
  }
250
- else if (optionValue) {
745
+ return detections;
746
+ }
747
+ function referencesSensitiveFile(line) {
748
+ return SENSITIVE_FILE_PATTERNS.some((pattern) => pattern.test(line));
749
+ }
750
+ function isSafeSensitiveHandlingInstruction(line) {
751
+ return (SAFE_NEGATION_RE.test(line) ||
752
+ /\b(never|do not|don't|avoid|exclude|skip)\b.{0,50}\b(upload|send|share|attach|copy|paste|include|print|cat|echo|log|dump)\b/i.test(line) ||
753
+ /\b(upload|send|share|attach|copy|paste|include|print|cat|echo|log|dump)\b.{0,50}\b(never|do not|don't|avoid|exclude|skip)\b/i.test(line));
754
+ }
755
+ function networkAndUploadDetections(line, lineNumber, policy) {
756
+ const detections = [];
757
+ if (EXTERNAL_UPLOAD_RE.test(line)) {
251
758
  detections.push({
252
- metadata: credentialArgMetadata,
759
+ metadata: RULES.externalUploadInstruction,
760
+ severity: policy.externalUploadAllowed === false ? "high" : "medium",
761
+ startLine: lineNumber,
762
+ snippet: line,
763
+ });
764
+ }
765
+ if (BULK_DATA_RE.test(line)) {
766
+ detections.push({
767
+ metadata: RULES.bulkDataSharingInstruction,
768
+ severity: "medium",
769
+ startLine: lineNumber,
770
+ snippet: line,
771
+ });
772
+ }
773
+ if (CLOUD_UPLOAD_RE.test(line) || CLOUD_DESTINATION_RE.test(line)) {
774
+ detections.push({
775
+ metadata: RULES.cloudUploadInstruction,
776
+ severity: "medium",
777
+ startLine: lineNumber,
778
+ snippet: line,
779
+ });
780
+ }
781
+ return detections;
782
+ }
783
+ function contextScopeDetections(line, lineNumber) {
784
+ const detections = [];
785
+ if (OVERBROAD_CONTEXT_RE.test(line)) {
786
+ detections.push({
787
+ metadata: RULES.overbroadContextInstruction,
253
788
  severity: "medium",
254
- confidence: "medium",
255
- line: 0,
789
+ startLine: lineNumber,
256
790
  snippet: line,
257
791
  });
258
792
  }
259
- const basicAuthMatch = line.match(/\s-u\s+(?:"([^"]+)"|'([^']+)'|(\S+:\S+))/i);
260
- const basicAuthValue = basicAuthMatch?.[1] ?? basicAuthMatch?.[2] ?? basicAuthMatch?.[3];
261
- if (basicAuthValue && basicAuthValue.includes(":")) {
793
+ if (NO_REDACTION_RE.test(line)) {
262
794
  detections.push({
263
- metadata: credentialArgMetadata,
264
- severity: isSafePlaceholder(basicAuthValue) ? "medium" : "high",
265
- confidence: isSafePlaceholder(basicAuthValue) ? "medium" : "high",
266
- line: 0,
795
+ metadata: RULES.noRedactionInstruction,
796
+ severity: "high",
797
+ startLine: lineNumber,
267
798
  snippet: line,
268
799
  });
269
800
  }
270
- const bearerMatch = line.match(/Authorization:\s*Bearer\s+(?:"([^"]+)"|'([^']+)'|(\S+))/i);
271
- const bearerValue = bearerMatch?.[1] ?? bearerMatch?.[2] ?? bearerMatch?.[3];
272
- if (bearerValue && !isSafePlaceholder(bearerValue)) {
801
+ return detections;
802
+ }
803
+ function commandDetections(line, lineNumber, hasApprovalGuard) {
804
+ const detections = [];
805
+ const remoteScript = line.match(REMOTE_SCRIPT_RE);
806
+ if (remoteScript && !hasPinnedRemoteScript(line)) {
807
+ const remoteUrl = (remoteScript[2] ?? line).replace(/[.,;:]+$/, "");
808
+ const shell = remoteScript[4] ?? remoteScript[5] ?? "sh";
809
+ detections.push({
810
+ metadata: RULES.unpinnedRemoteScript,
811
+ severity: "high",
812
+ startLine: lineNumber,
813
+ snippet: `curl ${remoteUrl} | ${shell}`,
814
+ dedupeKey: `${RULES.unpinnedRemoteScript.id}:${remoteUrl}`,
815
+ });
816
+ }
817
+ const unpinnedInstall = unpinnedDependencyInstall(line);
818
+ if (unpinnedInstall) {
273
819
  detections.push({
274
- metadata: credentialArgMetadata,
275
- severity: bearerValue === "..." ? "medium" : "high",
276
- confidence: bearerValue === "..." ? "medium" : "high",
277
- line: 0,
820
+ metadata: RULES.unpinnedDependencyInstall,
821
+ severity: "medium",
822
+ startLine: lineNumber,
278
823
  snippet: line,
279
824
  });
280
825
  }
281
- else if (bearerValue) {
826
+ if (PRIVILEGED_COMMAND_RE.test(line) && !hasApprovalGuard) {
282
827
  detections.push({
283
- metadata: credentialArgMetadata,
828
+ metadata: RULES.privilegedCommandWithoutGuard,
284
829
  severity: "medium",
285
- confidence: "medium",
286
- line: 0,
830
+ startLine: lineNumber,
831
+ snippet: line,
832
+ });
833
+ }
834
+ if (CREDENTIAL_ARG_RE.test(line) || CREDENTIAL_HEADER_RE.test(line)) {
835
+ detections.push({
836
+ metadata: RULES.credentialInCommandArg,
837
+ severity: "high",
838
+ startLine: lineNumber,
287
839
  snippet: line,
288
840
  });
289
841
  }
290
- return dedupeDetections(detections);
842
+ return detections;
291
843
  }
292
- function toFinding(path, detection) {
293
- return {
294
- ...detection.metadata,
295
- category: "safety",
296
- severity: detection.severity,
297
- confidence: detection.confidence,
298
- evidence: {
299
- path,
300
- startLine: detection.line,
301
- endLine: detection.line,
302
- snippet: detection.snippet,
844
+ function predictableTempDetections(line, lineNumber) {
845
+ const tempMatches = line.match(PREDICTABLE_TEMP_GLOBAL_RE) ?? [];
846
+ if (tempMatches.length === 0 ||
847
+ DESTRUCTIVE_COMMAND_RE.test(line) ||
848
+ /mktemp|tempfile|random|unique/i.test(line)) {
849
+ return [];
850
+ }
851
+ return [
852
+ {
853
+ metadata: RULES.predictableTempPath,
854
+ severity: sensitiveTempWords(line) ? "medium" : "low",
855
+ startLine: lineNumber,
856
+ snippet: line,
857
+ dedupeKey: `${RULES.predictableTempPath.id}:${tempMatches[0]}:${Math.floor((lineNumber - 1) / 10)}`,
303
858
  },
304
- constraints,
305
- verificationSteps,
306
- llmHint: detection.metadata.llmHint,
859
+ ];
860
+ }
861
+ function parseBlockList(lines, startIndex, scanEnd) {
862
+ const values = [];
863
+ let index = startIndex + 1;
864
+ for (; index < scanEnd; index += 1) {
865
+ const line = lines[index] ?? "";
866
+ if (line.trim().length === 0)
867
+ break;
868
+ const match = line.match(/^\s*-\s*(.*?)\s*$/);
869
+ if (!match)
870
+ break;
871
+ values.push(...parseList(match[1] ?? ""));
872
+ }
873
+ return { values, nextIndex: index };
874
+ }
875
+ function policyListValues(value, blockList) {
876
+ return value.length > 0 ? parseList(value) : blockList.values;
877
+ }
878
+ function consumesBlockList(value, blockList) {
879
+ return value.length === 0 && blockList.values.length > 0;
880
+ }
881
+ function isInlineListValue(value) {
882
+ return value.startsWith("[") || value.includes(",");
883
+ }
884
+ function parseSecurityPolicy(content) {
885
+ const policy = {
886
+ allowedData: [],
887
+ forbiddenInputs: [],
888
+ approvedNetworkDestinations: [],
889
+ approvedUploadDestinations: [],
890
+ disallowedCommands: [],
891
+ declared: new Set(),
892
+ lineByField: new Map(),
307
893
  };
894
+ const lines = content.split(/\r?\n/);
895
+ const frontmatterEnd = lines[0]?.trim() === "---"
896
+ ? lines.findIndex((line, index) => index > 0 && line.trim() === "---")
897
+ : -1;
898
+ const scanEnd = frontmatterEnd > 0 ? frontmatterEnd : Math.min(lines.length, 80);
899
+ for (let index = 0; index < scanEnd; index += 1) {
900
+ const line = lines[index] ?? "";
901
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*):\s*(.*?)\s*$/);
902
+ if (!match) {
903
+ continue;
904
+ }
905
+ const rawKey = match[1] ?? "";
906
+ const rawValue = match[2] ?? "";
907
+ const key = rawKey.trim();
908
+ const value = rawValue.trim();
909
+ const blockList = value.length === 0
910
+ ? parseBlockList(lines, index, scanEnd)
911
+ : { values: [], nextIndex: index + 1 };
912
+ const booleanField = BOOLEAN_POLICY_FIELDS.get(key);
913
+ if (booleanField !== undefined) {
914
+ const parsed = parseBoolean(value);
915
+ if (parsed !== undefined) {
916
+ policy[booleanField] = parsed;
917
+ policy.declared.add(booleanField);
918
+ policy.lineByField.set(booleanField, index + 1);
919
+ }
920
+ continue;
921
+ }
922
+ if (DESTINATION_POLICY_FIELDS.has(key)) {
923
+ const destinations = policyListValues(value, blockList);
924
+ policy.approvedNetworkDestinations.push(...destinations);
925
+ policy.declared.add("approvedNetworkDestinations");
926
+ policy.lineByField.set("approvedNetworkDestinations", index + 1);
927
+ if (consumesBlockList(value, blockList))
928
+ index = blockList.nextIndex - 1;
929
+ continue;
930
+ }
931
+ if (UPLOAD_DESTINATION_POLICY_FIELDS.has(key)) {
932
+ const destinations = policyListValues(value, blockList);
933
+ policy.approvedUploadDestinations.push(...destinations);
934
+ policy.declared.add("approvedUploadDestinations");
935
+ policy.lineByField.set("approvedUploadDestinations", index + 1);
936
+ if (consumesBlockList(value, blockList))
937
+ index = blockList.nextIndex - 1;
938
+ continue;
939
+ }
940
+ if (ALLOWED_DATA_POLICY_FIELDS.has(key)) {
941
+ const values = policyListValues(value, blockList);
942
+ if (value.length > 0 && !isInlineListValue(value)) {
943
+ policy.allowedDataClass = value;
944
+ }
945
+ else {
946
+ policy.allowedData.push(...values);
947
+ }
948
+ policy.declared.add("allowedData");
949
+ policy.lineByField.set("allowedData", index + 1);
950
+ if (consumesBlockList(value, blockList))
951
+ index = blockList.nextIndex - 1;
952
+ continue;
953
+ }
954
+ if (FORBIDDEN_INPUT_POLICY_FIELDS.has(key)) {
955
+ policy.forbiddenInputs.push(...policyListValues(value, blockList));
956
+ policy.declared.add("forbiddenInputs");
957
+ policy.lineByField.set("forbiddenInputs", index + 1);
958
+ if (consumesBlockList(value, blockList))
959
+ index = blockList.nextIndex - 1;
960
+ continue;
961
+ }
962
+ if (FUTURE_BLOCK_LIST_POLICY_FIELDS.has(key)) {
963
+ if (consumesBlockList(value, blockList))
964
+ index = blockList.nextIndex - 1;
965
+ continue;
966
+ }
967
+ if (SECURITY_PROFILE_POLICY_FIELDS.has(key)) {
968
+ policy.securityProfile = value;
969
+ policy.declared.add("securityProfile");
970
+ policy.lineByField.set("securityProfile", index + 1);
971
+ }
972
+ }
973
+ return policy;
308
974
  }
309
- function nearbyContext(lines, index) {
310
- const start = Math.max(0, index - 3);
311
- const end = Math.min(lines.length, index + 4);
312
- return lines.slice(start, end).join("\n").toLowerCase();
975
+ function applySecurityConfig(policy, config) {
976
+ if (config === undefined)
977
+ return policy;
978
+ const declared = new Set(policy.declared);
979
+ const lineByField = new Map(policy.lineByField);
980
+ const resolved = {
981
+ ...policy,
982
+ allowedData: [...policy.allowedData],
983
+ forbiddenInputs: [...policy.forbiddenInputs],
984
+ approvedNetworkDestinations: [...policy.approvedNetworkDestinations],
985
+ approvedUploadDestinations: [...policy.approvedUploadDestinations],
986
+ disallowedCommands: [...policy.disallowedCommands],
987
+ declared,
988
+ lineByField,
989
+ };
990
+ const chain = securityProfileChain(policy.securityProfile, config);
991
+ for (const item of chain.profiles) {
992
+ const profile = item.profile;
993
+ if (!declared.has("networkAllowed") &&
994
+ profile.networkAllowed !== undefined) {
995
+ resolved.networkAllowed = profile.networkAllowed;
996
+ }
997
+ if (!declared.has("externalUploadAllowed") &&
998
+ profile.externalUploadAllowed !== undefined) {
999
+ resolved.externalUploadAllowed = profile.externalUploadAllowed;
1000
+ }
1001
+ if (!declared.has("secretsAllowed") &&
1002
+ profile.secretsAllowed !== undefined) {
1003
+ resolved.secretsAllowed = profile.secretsAllowed;
1004
+ }
1005
+ if (!declared.has("humanApprovalRequired") &&
1006
+ profile.humanApprovalRequired !== undefined) {
1007
+ resolved.humanApprovalRequired = profile.humanApprovalRequired;
1008
+ }
1009
+ if (!declared.has("allowedData") &&
1010
+ profile.allowedDataClass !== undefined) {
1011
+ resolved.allowedDataClass = profile.allowedDataClass;
1012
+ }
1013
+ if (!declared.has("allowedData")) {
1014
+ resolved.allowedData.push(...profile.allowedData);
1015
+ }
1016
+ if (!declared.has("forbiddenInputs")) {
1017
+ resolved.forbiddenInputs.push(...profile.forbiddenInputs);
1018
+ }
1019
+ resolved.approvedNetworkDestinations.push(...profile.approvedDomains);
1020
+ resolved.approvedUploadDestinations.push(...profile.approvedUploadDomains);
1021
+ resolved.disallowedCommands.push(...profile.disallowedCommands);
1022
+ }
1023
+ resolved.approvedNetworkDestinations.push(...config.approvedDomains);
1024
+ resolved.approvedUploadDestinations.push(...config.approvedUploadDomains);
1025
+ resolved.disallowedCommands.push(...config.disallowedCommands);
1026
+ resolved.allowedData = uniqueStrings(resolved.allowedData);
1027
+ resolved.forbiddenInputs = uniqueStrings(resolved.forbiddenInputs);
1028
+ resolved.approvedNetworkDestinations = uniqueStrings(resolved.approvedNetworkDestinations);
1029
+ resolved.approvedUploadDestinations = uniqueStrings(resolved.approvedUploadDestinations);
1030
+ resolved.disallowedCommands = uniqueStrings(resolved.disallowedCommands);
1031
+ return resolved;
313
1032
  }
314
- function looksOperational(line) {
315
- if (line.length === 0)
316
- return false;
317
- if (line.startsWith("$ "))
318
- return true;
319
- if (/[|<>]/.test(line) &&
320
- /\b(curl|wget|bash|sh|tmp|Authorization)\b/i.test(line)) {
1033
+ function securityProfileChain(name, config) {
1034
+ if (name === undefined)
1035
+ return { profiles: [] };
1036
+ if (config === undefined)
1037
+ return { profiles: [], missingProfile: name };
1038
+ const profiles = [];
1039
+ const seen = new Set();
1040
+ const path = [];
1041
+ let current = name;
1042
+ while (current !== undefined) {
1043
+ if (seen.has(current)) {
1044
+ return {
1045
+ profiles: [],
1046
+ cycle: [...path.slice(path.indexOf(current)), current],
1047
+ };
1048
+ }
1049
+ seen.add(current);
1050
+ path.push(current);
1051
+ const profile = config.profiles?.[current];
1052
+ if (profile === undefined) {
1053
+ return { profiles: [], missingProfile: current };
1054
+ }
1055
+ profiles.push({ name: current, profile });
1056
+ current = profile.securityProfile;
1057
+ }
1058
+ return { profiles: profiles.reverse() };
1059
+ }
1060
+ function uniqueStrings(values) {
1061
+ return [...new Set(values)];
1062
+ }
1063
+ function effectiveAllowedDataClass(policy) {
1064
+ return policy.allowedDataClass;
1065
+ }
1066
+ function effectiveAllowedDataList(policy) {
1067
+ return policy.allowedData;
1068
+ }
1069
+ function securityPolicyResolutionDetections(parsedPolicy, resolvedPolicy, config, content) {
1070
+ const detections = [];
1071
+ if (parsedPolicy.securityProfile === undefined) {
1072
+ addForbiddenInputDetections(detections, resolvedPolicy, content);
1073
+ return detections;
1074
+ }
1075
+ const chain = securityProfileChain(parsedPolicy.securityProfile, config);
1076
+ const profileLine = parsedPolicy.lineByField.get("securityProfile") ?? 1;
1077
+ const profileSnippet = lineSnippet(content, profileLine) ??
1078
+ `security_profile: ${parsedPolicy.securityProfile}`;
1079
+ if (chain.missingProfile !== undefined) {
1080
+ detections.push({
1081
+ metadata: RULES.policyProfileNotFound,
1082
+ severity: "high",
1083
+ startLine: profileLine,
1084
+ snippet: profileSnippet,
1085
+ dedupeKey: `profile-not-found:${chain.missingProfile}`,
1086
+ });
1087
+ return detections;
1088
+ }
1089
+ if (chain.cycle !== undefined) {
1090
+ detections.push({
1091
+ metadata: RULES.policyProfileCycle,
1092
+ severity: "high",
1093
+ startLine: profileLine,
1094
+ snippet: profileSnippet,
1095
+ dedupeKey: `profile-cycle:${chain.cycle.join(">")}`,
1096
+ });
1097
+ return detections;
1098
+ }
1099
+ const inheritedNetworkAllowed = inheritedBoolean(chain, "networkAllowed");
1100
+ const inheritedUploadAllowed = inheritedBoolean(chain, "externalUploadAllowed");
1101
+ const inheritedSecretsAllowed = inheritedBoolean(chain, "secretsAllowed");
1102
+ const inheritedNetworkDestinations = chain.profiles.some((item) => item.profile.approvedDomains.length > 0);
1103
+ const inheritedUploadDestinations = chain.profiles.some((item) => item.profile.approvedUploadDomains.length > 0);
1104
+ addScalarOverrideContradiction(detections, parsedPolicy, content, "networkAllowed", inheritedNetworkAllowed, profileLine);
1105
+ addScalarOverrideContradiction(detections, parsedPolicy, content, "externalUploadAllowed", inheritedUploadAllowed, profileLine);
1106
+ addScalarOverrideContradiction(detections, parsedPolicy, content, "secretsAllowed", inheritedSecretsAllowed, profileLine);
1107
+ if (parsedPolicy.declared.has("networkAllowed") &&
1108
+ parsedPolicy.networkAllowed === false &&
1109
+ (inheritedNetworkAllowed ||
1110
+ inheritedNetworkDestinations ||
1111
+ resolvedPolicy.approvedNetworkDestinations.length >
1112
+ parsedPolicy.approvedNetworkDestinations.length)) {
1113
+ detections.push({
1114
+ metadata: RULES.policyOverrideContradiction,
1115
+ severity: "high",
1116
+ startLine: parsedPolicy.lineByField.get("networkAllowed") ?? profileLine,
1117
+ snippet: lineSnippet(content, parsedPolicy.lineByField.get("networkAllowed") ?? profileLine) ?? "network_allowed: false",
1118
+ dedupeKey: "override-contradiction:network",
1119
+ });
1120
+ }
1121
+ if (parsedPolicy.declared.has("externalUploadAllowed") &&
1122
+ parsedPolicy.externalUploadAllowed === false &&
1123
+ (inheritedUploadAllowed ||
1124
+ inheritedUploadDestinations ||
1125
+ resolvedPolicy.approvedUploadDestinations.length >
1126
+ parsedPolicy.approvedUploadDestinations.length)) {
1127
+ detections.push({
1128
+ metadata: RULES.policyOverrideContradiction,
1129
+ severity: "high",
1130
+ startLine: parsedPolicy.lineByField.get("externalUploadAllowed") ?? profileLine,
1131
+ snippet: lineSnippet(content, parsedPolicy.lineByField.get("externalUploadAllowed") ?? profileLine) ?? "external_upload_allowed: false",
1132
+ dedupeKey: "override-contradiction:upload",
1133
+ });
1134
+ }
1135
+ addForbiddenInputDetections(detections, resolvedPolicy, content);
1136
+ return detections;
1137
+ }
1138
+ function addForbiddenInputDetections(detections, policy, content) {
1139
+ for (const forbiddenInput of policy.forbiddenInputs) {
1140
+ const detection = forbiddenInputDetection(content, forbiddenInput);
1141
+ if (detection !== undefined)
1142
+ detections.push(detection);
1143
+ }
1144
+ }
1145
+ function inheritedBoolean(chain, field) {
1146
+ for (let index = chain.profiles.length - 1; index >= 0; index -= 1) {
1147
+ const value = chain.profiles[index]?.profile[field];
1148
+ if (value !== undefined)
1149
+ return value;
1150
+ }
1151
+ return undefined;
1152
+ }
1153
+ function addScalarOverrideContradiction(detections, parsedPolicy, content, field, inheritedValue, fallbackLine) {
1154
+ const artifactValue = parsedPolicy[field];
1155
+ if (inheritedValue === false &&
1156
+ artifactValue === true &&
1157
+ parsedPolicy.declared.has(field)) {
1158
+ pushOverrideContradiction(detections, parsedPolicy, content, field, fallbackLine);
1159
+ }
1160
+ }
1161
+ function pushOverrideContradiction(detections, parsedPolicy, content, field, fallbackLine) {
1162
+ const line = parsedPolicy.lineByField.get(field) ?? fallbackLine;
1163
+ detections.push({
1164
+ metadata: RULES.policyOverrideContradiction,
1165
+ severity: "high",
1166
+ startLine: line,
1167
+ snippet: lineSnippet(content, line) ?? field,
1168
+ dedupeKey: `override-contradiction:${field}`,
1169
+ });
1170
+ }
1171
+ function forbiddenInputDetection(content, forbiddenInput) {
1172
+ const needle = forbiddenInput.trim();
1173
+ if (needle.length === 0)
1174
+ return undefined;
1175
+ const pattern = new RegExp(`\\b${escapeRegExp(needle)}\\b`, "i");
1176
+ const lines = content.split(/\r?\n/);
1177
+ const frontmatterEnd = lines[0]?.trim() === "---"
1178
+ ? lines.findIndex((line, index) => index > 0 && line.trim() === "---")
1179
+ : -1;
1180
+ const scanStart = frontmatterEnd > 0 ? frontmatterEnd + 1 : 0;
1181
+ for (let index = scanStart; index < lines.length; index += 1) {
1182
+ const line = lines[index] ?? "";
1183
+ if (!pattern.test(line))
1184
+ continue;
1185
+ if (SAFE_FORBIDDEN_INPUT_PATTERN.test(line))
1186
+ continue;
1187
+ if (!FORBIDDEN_INPUT_ACTION_PATTERN.test(line))
1188
+ continue;
1189
+ return {
1190
+ metadata: RULES.forbiddenInputInstruction,
1191
+ severity: "high",
1192
+ startLine: index + 1,
1193
+ snippet: line.trim(),
1194
+ dedupeKey: `forbidden-input:${needle.toLowerCase()}`,
1195
+ };
1196
+ }
1197
+ return undefined;
1198
+ }
1199
+ function lineSnippet(content, line) {
1200
+ return content.split(/\r?\n/)[line - 1]?.trim();
1201
+ }
1202
+ function parseBoolean(value) {
1203
+ const normalized = value.toLowerCase();
1204
+ if (["true", "yes", "allowed", "allow", "1"].includes(normalized)) {
321
1205
  return true;
322
1206
  }
323
- return commandStarters.some((command) => new RegExp(`^(?:[-*]\\s+)?${command}\\b`, "i").test(line));
1207
+ if (["false", "no", "denied", "deny", "0"].includes(normalized)) {
1208
+ return false;
1209
+ }
1210
+ return undefined;
1211
+ }
1212
+ function parseList(value) {
1213
+ return value
1214
+ .replace(/^\[/, "")
1215
+ .replace(/\]$/, "")
1216
+ .split(",")
1217
+ .map((item) => item.trim().replace(/^["']|["']$/g, ""))
1218
+ .filter((item) => item.length > 0);
1219
+ }
1220
+ function unapprovedNetworkDestinations(line, policy) {
1221
+ return unapprovedDestinations(line, policy.approvedNetworkDestinations);
1222
+ }
1223
+ function unapprovedDestinations(line, approvedDestinations) {
1224
+ const approved = approvedDestinations
1225
+ .map((destination) => normalizeNetworkDestination(destination))
1226
+ .filter((destination) => destination !== undefined);
1227
+ if (approved.length === 0) {
1228
+ return [];
1229
+ }
1230
+ return extractNetworkDestinations(line).filter((destination) => !approved.some((approvedDestination) => networkDestinationMatches(destination, approvedDestination)));
324
1231
  }
325
- function isCommentOnlyShellLine(line) {
326
- return line.startsWith("#");
1232
+ function isUploadInstruction(line) {
1233
+ return (EXTERNAL_UPLOAD_RE.test(line) ||
1234
+ CLOUD_UPLOAD_RE.test(line) ||
1235
+ (UPLOAD_DESTINATION_ACTION_RE.test(line) &&
1236
+ extractNetworkDestinations(line).length > 0));
327
1237
  }
328
- function collapsePredictableTempPathFindings(findings) {
329
- const collapsed = [];
330
- const lastByPath = new Map();
331
- for (const finding of findings) {
332
- if (finding.id !== "SEC-PREDICTABLE-TEMP-PATH") {
333
- collapsed.push(finding);
1238
+ function extractNetworkDestinations(line) {
1239
+ const seen = new Set();
1240
+ const destinations = [];
1241
+ for (const match of line.matchAll(NETWORK_DESTINATION_RE)) {
1242
+ const destination = normalizeNetworkDestination(match[0] ?? "");
1243
+ if (destination === undefined) {
334
1244
  continue;
335
1245
  }
336
- const tempPath = extractTempPath(finding.evidence.snippet);
337
- if (!tempPath) {
338
- collapsed.push(finding);
1246
+ const key = destination.host + destination.path;
1247
+ if (seen.has(key)) {
339
1248
  continue;
340
1249
  }
341
- const key = `${finding.evidence.path}:${tempPath}`;
342
- const previous = lastByPath.get(key);
343
- if (previous &&
344
- finding.evidence.startLine - previous.evidence.startLine <= 10) {
345
- continue;
1250
+ seen.add(key);
1251
+ destinations.push(destination);
1252
+ }
1253
+ return destinations;
1254
+ }
1255
+ function normalizeNetworkDestination(candidate) {
1256
+ const raw = candidate.trim().replace(TRAILING_DESTINATION_PUNCTUATION_RE, "");
1257
+ if (raw.length === 0) {
1258
+ return undefined;
1259
+ }
1260
+ const parseable = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw)
1261
+ ? raw
1262
+ : `https://${raw}`;
1263
+ try {
1264
+ const url = new URL(parseable);
1265
+ const host = url.hostname.toLowerCase();
1266
+ if (!host.includes(".")) {
1267
+ return undefined;
346
1268
  }
347
- lastByPath.set(key, finding);
348
- collapsed.push(finding);
1269
+ const path = url.pathname.replace(/\/+$/, "");
1270
+ return {
1271
+ raw,
1272
+ host,
1273
+ path: path === "/" ? "" : path,
1274
+ };
1275
+ }
1276
+ catch {
1277
+ return undefined;
349
1278
  }
350
- return collapsed;
351
1279
  }
352
- function extractTempPath(value) {
353
- return value.match(/\/tmp\/[A-Za-z0-9._/-]+/)?.[0];
1280
+ function networkDestinationMatches(candidate, approved) {
1281
+ if (approved.path.length > 0) {
1282
+ return (candidate.host === approved.host &&
1283
+ (candidate.path === approved.path ||
1284
+ candidate.path.startsWith(`${approved.path}/`)));
1285
+ }
1286
+ return (candidate.host === approved.host ||
1287
+ candidate.host.endsWith(`.${approved.host}`));
1288
+ }
1289
+ function matchesDisallowedCommand(line, command) {
1290
+ const normalizedLine = line.toLowerCase().replace(/\s+/g, " ");
1291
+ const normalizedCommand = command.trim().toLowerCase().replace(/\s+/g, " ");
1292
+ if (normalizedCommand.length === 0)
1293
+ return false;
1294
+ if (/^[a-z0-9_-]+$/.test(normalizedCommand)) {
1295
+ const escaped = escapeRegExp(normalizedCommand);
1296
+ return new RegExp(`(^|[^a-z0-9_-])${escaped}($|[^a-z0-9_-])`).test(normalizedLine);
1297
+ }
1298
+ return normalizedLine.includes(normalizedCommand);
1299
+ }
1300
+ function escapeRegExp(value) {
1301
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1302
+ }
1303
+ function isPolicyLine(line) {
1304
+ const key = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*):/)?.[1];
1305
+ return (key !== undefined &&
1306
+ (BOOLEAN_POLICY_FIELDS.has(key) ||
1307
+ DESTINATION_POLICY_FIELDS.has(key) ||
1308
+ UPLOAD_DESTINATION_POLICY_FIELDS.has(key) ||
1309
+ ALLOWED_DATA_POLICY_FIELDS.has(key) ||
1310
+ FORBIDDEN_INPUT_POLICY_FIELDS.has(key) ||
1311
+ SECURITY_PROFILE_POLICY_FIELDS.has(key) ||
1312
+ FUTURE_BLOCK_LIST_POLICY_FIELDS.has(key)));
1313
+ }
1314
+ function isCommandLike(line) {
1315
+ return /\b(npm|pnpm|yarn|pip3?|brew|docker|curl|wget|sudo|chmod|chown|git|gh|aws|gcloud|az|kubectl|echo|cat|cp|mv|rm|touch|mkdir)\b/i.test(line);
1316
+ }
1317
+ function hasNearbyApprovalLanguage(line) {
1318
+ return (APPROVAL_RE.test(line) ||
1319
+ /\b(approved|approval|confirm|confirmed|ask|asking|asked|review|reviewed|authorize|authorized|permission|consent|guardrail|guard)\b/i.test(line));
1320
+ }
1321
+ function hasPinnedRemoteScript(line) {
1322
+ return /\b(sha256|sha512|checksum|gpg|cosign|sigstore|version|v\d+\.\d+\.\d+|@[a-f0-9]{7,40})\b/i.test(line);
1323
+ }
1324
+ function unpinnedDependencyInstall(line) {
1325
+ const npm = line.match(/\bnpm\s+(?:install|i|add)\s+([^\n#]+)/i);
1326
+ if (npm && splitCommandArgs(npm[1] ?? "").some(isUnpinnedNpmPackage)) {
1327
+ return true;
1328
+ }
1329
+ const pnpm = line.match(/\bpnpm\s+(?:add|install)\s+([^\n#]+)/i);
1330
+ if (pnpm && splitCommandArgs(pnpm[1] ?? "").some(isUnpinnedNpmPackage)) {
1331
+ return true;
1332
+ }
1333
+ const yarn = line.match(/\byarn\s+add\s+([^\n#]+)/i);
1334
+ if (yarn && splitCommandArgs(yarn[1] ?? "").some(isUnpinnedNpmPackage)) {
1335
+ return true;
1336
+ }
1337
+ const pip = line.match(/\bpip3?\s+install\s+([^\n#]+)/i);
1338
+ if (pip && splitCommandArgs(pip[1] ?? "").some(isUnpinnedPythonPackage)) {
1339
+ return true;
1340
+ }
1341
+ const brew = line.match(/\bbrew\s+install\s+([^\n#]+)/i);
1342
+ if (brew && splitCommandArgs(brew[1] ?? "").some(isUnpinnedBrewFormula)) {
1343
+ return true;
1344
+ }
1345
+ const docker = line.match(/\bdocker\s+(?:pull|run)\s+([^\s#]+)/i);
1346
+ if (docker && isUnpinnedContainerImage(docker[1] ?? "")) {
1347
+ return true;
1348
+ }
1349
+ return false;
354
1350
  }
355
1351
  function splitCommandArgs(value) {
356
1352
  return value
357
1353
  .split(/\s+/)
358
- .map((arg) => arg.replace(/^["']|["']$/g, ""))
359
- .filter(Boolean);
1354
+ .map((arg) => arg.trim())
1355
+ .filter((arg) => arg.length > 0 &&
1356
+ !arg.startsWith("-") &&
1357
+ !arg.includes("=") &&
1358
+ !/[|;&]/.test(arg));
1359
+ }
1360
+ function isUnpinnedNpmPackage(arg) {
1361
+ if (isPlaceholder(arg) || arg.startsWith("$") || arg.startsWith(".")) {
1362
+ return false;
1363
+ }
1364
+ const packageName = arg.startsWith("@")
1365
+ ? arg.split("@").slice(0, 2).join("@")
1366
+ : arg.split("@")[0];
1367
+ return packageName === arg;
360
1368
  }
361
- function isPackageToken(arg) {
362
- return (!arg.startsWith("-") && !arg.includes("/") && arg !== "." && arg !== "..");
1369
+ function isUnpinnedPythonPackage(arg) {
1370
+ if (isPlaceholder(arg) || arg.startsWith("-") || arg.startsWith(".")) {
1371
+ return false;
1372
+ }
1373
+ return !/[=<>~!]=|===/.test(arg);
363
1374
  }
364
- function isPinnedNpmPackage(arg) {
365
- if (arg.startsWith("@")) {
366
- const lastAt = arg.lastIndexOf("@");
367
- return lastAt > 0 && lastAt < arg.length - 1;
1375
+ function isUnpinnedBrewFormula(arg) {
1376
+ if (isPlaceholder(arg) || arg.includes("/")) {
1377
+ return false;
368
1378
  }
369
- return /@\d/.test(arg);
1379
+ return !arg.includes("@");
370
1380
  }
371
- function hasGuardrail(context) {
372
- return guardWords.some((word) => context.includes(word));
1381
+ function isUnpinnedContainerImage(image) {
1382
+ if (isPlaceholder(image)) {
1383
+ return false;
1384
+ }
1385
+ const tag = image.includes(":") ? image.split(":").pop() : undefined;
1386
+ return tag === undefined || tag === "" || tag === "latest";
373
1387
  }
374
- function isSafePlaceholder(value) {
375
- const normalized = value.trim().replace(/[",]$/g, "");
376
- return (normalized.startsWith("$") ||
377
- /^<[^>]+>$/.test(normalized) ||
378
- /^YOUR_[A-Z0-9_]+$/.test(normalized) ||
379
- /^REDACTED$/i.test(normalized) ||
380
- /^\*+$/.test(normalized));
1388
+ function isPlaceholder(value) {
1389
+ return /^<.*>$|^\[.*\]$|^(example|placeholder|package|image)$/i.test(value);
1390
+ }
1391
+ function sensitiveTempWords(line) {
1392
+ return /\b(profile|credential|credentials|secret|token|password|cert|certificate|key|signing|auth|cookie|session|log|dump)\b|\/tmp\/(?:token|secret)\b|\/tmp\/[^/\s]+\.plist\b/i.test(line);
381
1393
  }
382
1394
  function dedupeDetections(detections) {
383
1395
  const seen = new Set();
384
- return detections.filter((detection) => {
385
- const key = `${detection.metadata.id}:${detection.snippet}`;
386
- if (seen.has(key))
387
- return false;
388
- seen.add(key);
389
- return true;
390
- });
1396
+ const unique = [];
1397
+ for (const detection of detections) {
1398
+ const key = detection.dedupeKey ?? `${detection.metadata.id}:${detection.snippet}`;
1399
+ if (!seen.has(key)) {
1400
+ seen.add(key);
1401
+ unique.push(detection);
1402
+ }
1403
+ }
1404
+ return unique;
1405
+ }
1406
+ function findingFromDetection(artifact, detection) {
1407
+ return {
1408
+ id: detection.metadata.id,
1409
+ severity: detection.severity,
1410
+ category: detection.metadata.category,
1411
+ title: detection.metadata.title,
1412
+ evidence: {
1413
+ path: artifact.path,
1414
+ startLine: detection.startLine,
1415
+ endLine: detection.endLine ?? detection.startLine,
1416
+ snippet: snippet(detection.snippet),
1417
+ },
1418
+ whyItMatters: detection.metadata.whyItMatters,
1419
+ remediation: detection.metadata.remediation,
1420
+ constraints: detection.metadata.constraints,
1421
+ verificationSteps: detection.metadata.verificationSteps,
1422
+ llmHint: detection.metadata.llmHint,
1423
+ confidence: detection.metadata.confidence,
1424
+ };
391
1425
  }
392
- function compareDiagnostics(a, b) {
393
- const aPath = a.evidence.path;
394
- const bPath = b.evidence.path;
395
- if (aPath !== bPath)
396
- return aPath.localeCompare(bPath);
397
- const aLine = a.evidence.startLine;
398
- const bLine = b.evidence.startLine;
399
- if (aLine !== bLine)
400
- return aLine - bLine;
401
- return a.id.localeCompare(b.id);
1426
+ function snippet(value) {
1427
+ return value.trim().replace(/\s+/g, " ").slice(0, 240);
402
1428
  }
403
1429
  //# sourceMappingURL=security-diagnostics.js.map