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.
- package/README.md +61 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +78 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/graph.d.ts +1 -0
- package/dist/commands/graph.d.ts.map +1 -1
- package/dist/commands/graph.js +34 -1
- package/dist/commands/graph.js.map +1 -1
- package/dist/commands/scaffold.d.ts +25 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/scaffold.js +208 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +119 -0
- package/dist/config.js.map +1 -1
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +29 -3
- package/dist/markdown.js.map +1 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +10 -4
- package/dist/metadata.js.map +1 -1
- package/dist/rules.d.ts.map +1 -1
- package/dist/rules.js +9 -4
- package/dist/rules.js.map +1 -1
- package/dist/scanner.js +1 -1
- package/dist/scanner.js.map +1 -1
- package/dist/security-diagnostics.d.ts +6 -2
- package/dist/security-diagnostics.d.ts.map +1 -1
- package/dist/security-diagnostics.js +1327 -301
- package/dist/security-diagnostics.js.map +1 -1
- package/dist/types.d.ts +22 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,403 +1,1429 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
533
|
+
if (isPolicyLine(line)) {
|
|
103
534
|
continue;
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
560
|
+
detections.push(...policyContradictions(policy));
|
|
561
|
+
return dedupeDetections(detections).map((detection) => findingFromDetection(artifact, detection));
|
|
118
562
|
}
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
129
|
-
severity:
|
|
130
|
-
|
|
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
|
|
137
|
-
if (!operational)
|
|
138
|
-
return [];
|
|
577
|
+
function bodyPolicyContradictionDetections(content, policy) {
|
|
139
578
|
const detections = [];
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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:
|
|
149
|
-
severity:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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:
|
|
163
|
-
severity: "
|
|
164
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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:
|
|
176
|
-
severity: "
|
|
177
|
-
|
|
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 (
|
|
668
|
+
if (policy.secretsAllowed === false &&
|
|
669
|
+
SECRET_WORD_RE.test(line) &&
|
|
670
|
+
!SAFE_NEGATION_RE.test(line)) {
|
|
184
671
|
detections.push({
|
|
185
|
-
metadata:
|
|
186
|
-
severity: "
|
|
187
|
-
|
|
188
|
-
line: 0,
|
|
672
|
+
metadata: RULES.instructionViolatesPolicy,
|
|
673
|
+
severity: "high",
|
|
674
|
+
startLine: lineNumber,
|
|
189
675
|
snippet: line,
|
|
190
676
|
});
|
|
191
677
|
}
|
|
192
|
-
if (
|
|
678
|
+
if (effectiveAllowedDataClass(policy)?.toLowerCase() === "disclosed" &&
|
|
679
|
+
UNDISCLOSED_DATA_RE.test(line)) {
|
|
193
680
|
detections.push({
|
|
194
|
-
metadata:
|
|
195
|
-
severity: "
|
|
196
|
-
|
|
197
|
-
line: 0,
|
|
681
|
+
metadata: RULES.instructionViolatesPolicy,
|
|
682
|
+
severity: "high",
|
|
683
|
+
startLine: lineNumber,
|
|
198
684
|
snippet: line,
|
|
199
685
|
});
|
|
200
686
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
216
|
-
line: 0,
|
|
694
|
+
startLine: lineNumber,
|
|
217
695
|
snippet: line,
|
|
218
|
-
}
|
|
219
|
-
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
return detections;
|
|
220
699
|
}
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
236
|
-
if (!operational)
|
|
237
|
-
return [];
|
|
721
|
+
function sensitiveDataDetections(line, lineNumber, policy) {
|
|
238
722
|
const detections = [];
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
if (optionValue && !isSafePlaceholder(optionValue)) {
|
|
723
|
+
const sensitiveFile = referencesSensitiveFile(line);
|
|
724
|
+
if (sensitiveFile && !isSafeSensitiveHandlingInstruction(line)) {
|
|
242
725
|
detections.push({
|
|
243
|
-
metadata:
|
|
726
|
+
metadata: RULES.sensitiveFileReference,
|
|
244
727
|
severity: "high",
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
255
|
-
line: 0,
|
|
789
|
+
startLine: lineNumber,
|
|
256
790
|
snippet: line,
|
|
257
791
|
});
|
|
258
792
|
}
|
|
259
|
-
|
|
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:
|
|
264
|
-
severity:
|
|
265
|
-
|
|
266
|
-
line: 0,
|
|
795
|
+
metadata: RULES.noRedactionInstruction,
|
|
796
|
+
severity: "high",
|
|
797
|
+
startLine: lineNumber,
|
|
267
798
|
snippet: line,
|
|
268
799
|
});
|
|
269
800
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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:
|
|
275
|
-
severity:
|
|
276
|
-
|
|
277
|
-
line: 0,
|
|
820
|
+
metadata: RULES.unpinnedDependencyInstall,
|
|
821
|
+
severity: "medium",
|
|
822
|
+
startLine: lineNumber,
|
|
278
823
|
snippet: line,
|
|
279
824
|
});
|
|
280
825
|
}
|
|
281
|
-
|
|
826
|
+
if (PRIVILEGED_COMMAND_RE.test(line) && !hasApprovalGuard) {
|
|
282
827
|
detections.push({
|
|
283
|
-
metadata:
|
|
828
|
+
metadata: RULES.privilegedCommandWithoutGuard,
|
|
284
829
|
severity: "medium",
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
842
|
+
return detections;
|
|
291
843
|
}
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
315
|
-
if (
|
|
316
|
-
return
|
|
317
|
-
if (
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
|
326
|
-
return
|
|
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
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
for (const
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
337
|
-
if (
|
|
338
|
-
collapsed.push(finding);
|
|
1246
|
+
const key = destination.host + destination.path;
|
|
1247
|
+
if (seen.has(key)) {
|
|
339
1248
|
continue;
|
|
340
1249
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
|
353
|
-
|
|
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.
|
|
359
|
-
.filter(
|
|
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
|
|
362
|
-
|
|
1369
|
+
function isUnpinnedPythonPackage(arg) {
|
|
1370
|
+
if (isPlaceholder(arg) || arg.startsWith("-") || arg.startsWith(".")) {
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
return !/[=<>~!]=|===/.test(arg);
|
|
363
1374
|
}
|
|
364
|
-
function
|
|
365
|
-
if (arg.
|
|
366
|
-
|
|
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
|
|
1379
|
+
return !arg.includes("@");
|
|
370
1380
|
}
|
|
371
|
-
function
|
|
372
|
-
|
|
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
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
393
|
-
|
|
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
|