composable-data-stack 0.4.0__py3-none-any.whl

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.
@@ -0,0 +1,919 @@
1
+ {
2
+ "$schema": "rule-schema.json",
3
+ "version": "1.0.0",
4
+ "metadata": {
5
+ "name": "cds-core-security-rules",
6
+ "description": "Core security validation rules for cds validate --security",
7
+ "defaultProfileClass": "local"
8
+ },
9
+ "rules": [
10
+ {
11
+ "id": "CDS-SEC-001",
12
+ "title": "Hardcoded secret-like value",
13
+ "severity": "high",
14
+ "category": "secrets",
15
+ "scope": ["profile-raw", "module-values"],
16
+ "match": {
17
+ "all": [
18
+ {
19
+ "keyRegex": "(?i)(password|passwd|secret|token|api[_-]?key|access[_-]?key|secret[_-]?key|private[_-]?key|jwt|dsn)"
20
+ },
21
+ {
22
+ "notValueRegex": "^(\\$\\{[A-Z0-9_:-]+\\}|<set-me>|<from-env>|REDACTED|example)$"
23
+ }
24
+ ]
25
+ },
26
+ "message": "Secret-like value appears to be hardcoded",
27
+ "whyItMatters": "Hardcoded secrets are easy to leak via git, logs, and rendered artifacts.",
28
+ "recommendation": [
29
+ "Move the value to environment variables or a secret backend.",
30
+ "Replace literal values with references."
31
+ ],
32
+ "enabled": true
33
+ },
34
+ {
35
+ "id": "CDS-SEC-002",
36
+ "title": "Secret embedded in URL or DSN",
37
+ "severity": "high",
38
+ "category": "secrets",
39
+ "scope": ["profile-raw", "profile-resolved", "service-env"],
40
+ "match": {
41
+ "any": [
42
+ {
43
+ "valueRegex": "^[a-zA-Z][a-zA-Z0-9+.-]*://[^\\s:@/]+:[^\\s@/]+@[^\\s]+$"
44
+ },
45
+ {
46
+ "valueRegex": "(?i)(token|apikey|api_key|access_token)=[^&\\s]+"
47
+ }
48
+ ]
49
+ },
50
+ "message": "Credential material appears embedded in a connection string or URL",
51
+ "whyItMatters": "Secrets in URLs are commonly exposed through logs, process listings, and config snapshots.",
52
+ "recommendation": [
53
+ "Move credentials out of DSNs and inject them separately.",
54
+ "Avoid query-string tokens in configuration."
55
+ ],
56
+ "enabled": true
57
+ },
58
+ {
59
+ "id": "CDS-SEC-003",
60
+ "title": "Inline private key or high-entropy secret material",
61
+ "severity": "high",
62
+ "category": "secrets",
63
+ "scope": ["profile-raw", "module-values"],
64
+ "match": {
65
+ "any": [
66
+ {
67
+ "valueRegex": "-----BEGIN (RSA )?PRIVATE KEY-----"
68
+ },
69
+ {
70
+ "entropy": "high",
71
+ "minLength": 24
72
+ }
73
+ ]
74
+ },
75
+ "message": "Private key material or secret-like high-entropy value appears inline",
76
+ "whyItMatters": "Inline key material is highly sensitive and difficult to control once committed or rendered.",
77
+ "recommendation": [
78
+ "Store private keys in a secret backend or mounted secret file.",
79
+ "Do not keep cryptographic material directly in profile YAML."
80
+ ],
81
+ "enabled": true
82
+ },
83
+ {
84
+ "id": "CDS-SEC-004",
85
+ "title": "Default or placeholder secret in non-local profile",
86
+ "severity": "high",
87
+ "category": "secrets",
88
+ "scope": ["profile-raw", "profile-resolved", "service-env"],
89
+ "match": {
90
+ "all": [
91
+ {
92
+ "profileClasses": ["dev", "staging", "prod"]
93
+ },
94
+ {
95
+ "equalsAny": [
96
+ "admin",
97
+ "admin123",
98
+ "password",
99
+ "password123",
100
+ "postgres",
101
+ "postgres123",
102
+ "changeme",
103
+ "change-me",
104
+ "secret",
105
+ "default",
106
+ "test",
107
+ "superset"
108
+ ],
109
+ "keyRegex": "(?i)(password|secret|token|key|credential|passwd|pwd)"
110
+ }
111
+ ]
112
+ },
113
+ "message": "Weak default or placeholder secret value detected",
114
+ "whyItMatters": "Default credentials are a common first target and are frequently exploited automatically.",
115
+ "recommendation": [
116
+ "Generate unique secrets for each environment.",
117
+ "Reject placeholder values outside local-only demo profiles."
118
+ ],
119
+ "enabled": true
120
+ },
121
+ {
122
+ "id": "CDS-SEC-005",
123
+ "title": "Secret interpolated into non-secret field",
124
+ "severity": "high",
125
+ "category": "secrets",
126
+ "scope": ["profile-resolved", "bindings"],
127
+ "match": {
128
+ "all": [
129
+ {
130
+ "envInterpolation": true
131
+ },
132
+ {
133
+ "pathPatterns": [
134
+ "services.*.labels.*",
135
+ "services.*.annotations.*",
136
+ "services.*.command",
137
+ "services.*.args.*",
138
+ "outputs.*",
139
+ "plan.preview.*"
140
+ ]
141
+ }
142
+ ]
143
+ },
144
+ "message": "Secret reference appears to flow into a non-secret sink",
145
+ "whyItMatters": "Non-secret fields are often surfaced in logs, metadata, or UI output.",
146
+ "recommendation": [
147
+ "Restrict secrets to approved secret sinks such as env refs or secret files.",
148
+ "Do not inject secrets into labels, command arguments, or annotations."
149
+ ],
150
+ "enabled": true
151
+ },
152
+ {
153
+ "id": "CDS-SEC-006",
154
+ "$comment": "Deferred: this rule is about secrets leaking into rendered Compose, generated artifacts, or CLI text output after resolution. The current scanner only flattens profile inputs and loaded .env secrets, so enabling a non-'none' scope here would still never inspect the output surfaces this rule names.",
155
+ "title": "Resolved secret present in generated or CLI output",
156
+ "severity": "high",
157
+ "category": "secrets",
158
+ "scope": ["none"],
159
+ "match": {
160
+ "any": [
161
+ {
162
+ "keyRegex": "(?i)(password|passwd|secret|token|api[_-]?key|private[_-]?key|dsn)"
163
+ },
164
+ {
165
+ "entropy": "high",
166
+ "minLength": 16
167
+ }
168
+ ]
169
+ },
170
+ "message": "Rendered or planned output appears to contain a resolved secret",
171
+ "whyItMatters": "Generated artifacts are often stored, shared, or captured by CI systems.",
172
+ "recommendation": [
173
+ "Redact secrets in plan and render output.",
174
+ "Avoid writing resolved secret values to generated files."
175
+ ],
176
+ "enabled": true
177
+ },
178
+ {
179
+ "id": "CDS-SEC-010",
180
+ "title": "Default admin credentials",
181
+ "severity": "high",
182
+ "category": "auth",
183
+ "scope": ["profile-resolved", "service-env"],
184
+ "match": {
185
+ "any": [
186
+ {
187
+ "pathPatterns": [
188
+ "services.*.environment.ADMIN_USERNAME",
189
+ "services.*.environment.ADMIN_PASSWORD",
190
+ "services.*.environment.POSTGRES_USER",
191
+ "services.*.environment.POSTGRES_PASSWORD"
192
+ ],
193
+ "equalsAny": [
194
+ "admin",
195
+ "password",
196
+ "postgres",
197
+ "superset"
198
+ ]
199
+ }
200
+ ]
201
+ },
202
+ "message": "Known default administrative credentials detected",
203
+ "whyItMatters": "Attackers routinely try default credentials first.",
204
+ "recommendation": [
205
+ "Set unique admin credentials before deployment.",
206
+ "Disallow well-known defaults in validation."
207
+ ],
208
+ "enabled": true
209
+ },
210
+ {
211
+ "id": "CDS-SEC-011",
212
+ "title": "Missing required authentication secret",
213
+ "severity": "high",
214
+ "category": "auth",
215
+ "scope": ["profile-resolved", "service-env"],
216
+ "match": {
217
+ "all": [
218
+ {
219
+ "pathPatterns": [
220
+ "services.*.environment.SUPERSET_SECRET_KEY",
221
+ "services.*.environment.POSTGRES_PASSWORD",
222
+ "services.*.environment.ADMIN_PASSWORD",
223
+ "services.*.environment.JWT_SECRET"
224
+ ],
225
+ "profileClasses": ["dev", "staging", "prod"]
226
+ },
227
+ {
228
+ "allowEmpty": true
229
+ }
230
+ ]
231
+ },
232
+ "message": "A required authentication secret appears missing or empty",
233
+ "whyItMatters": "Missing auth secrets commonly trigger insecure defaults or prevent security controls from working.",
234
+ "recommendation": [
235
+ "Provide the required secret through environment or a secret store.",
236
+ "Fail validation when required auth material is missing."
237
+ ],
238
+ "enabled": true
239
+ },
240
+ {
241
+ "id": "CDS-SEC-012",
242
+ "title": "Weak secret strength",
243
+ "severity": "high",
244
+ "category": "auth",
245
+ "scope": ["profile-raw", "profile-resolved", "service-env"],
246
+ "match": {
247
+ "any": [
248
+ {
249
+ "keyRegex": "(?i)(password|secret|token|key|credential|passwd|pwd|auth|api[-_]?key|access[-_]?key)",
250
+ "minLength": 1,
251
+ "valueRegex": "^.{1,11}$"
252
+ },
253
+ {
254
+ "keyRegex": "(?i)(password|secret|token|key|credential|passwd|pwd|auth|api[-_]?key|access[-_]?key)",
255
+ "equalsAny": [
256
+ "admin",
257
+ "password",
258
+ "secret",
259
+ "changeme",
260
+ "change-me",
261
+ "default",
262
+ "test123"
263
+ ]
264
+ }
265
+ ]
266
+ },
267
+ "message": "Secret value appears too weak or predictable",
268
+ "whyItMatters": "Short or predictable secrets can be guessed or brute-forced easily.",
269
+ "recommendation": [
270
+ "Require randomly generated secrets of sufficient length.",
271
+ "Reject dictionary-like or commonly used values."
272
+ ],
273
+ "enabled": true
274
+ },
275
+ {
276
+ "id": "CDS-SEC-013",
277
+ "title": "Secret reused across unrelated services",
278
+ "severity": "medium",
279
+ "category": "auth",
280
+ "scope": ["profile-resolved", "service-env"],
281
+ "match": {
282
+ "all": [
283
+ {
284
+ "keyRegex": "(?i)(password|secret|token|key)"
285
+ },
286
+ {
287
+ "valueRegex": ".+"
288
+ }
289
+ ]
290
+ },
291
+ "message": "The same secret appears reused across multiple services",
292
+ "whyItMatters": "Credential reuse increases blast radius if one service is compromised.",
293
+ "recommendation": [
294
+ "Use separate credentials or secrets per service.",
295
+ "Generate scoped secrets rather than sharing one across components."
296
+ ],
297
+ "enabled": false
298
+ },
299
+ {
300
+ "id": "CDS-SEC-020",
301
+ "title": "Admin or UI service bound to 0.0.0.0",
302
+ "severity": "high",
303
+ "category": "network-exposure",
304
+ "scope": ["service"],
305
+ "match": {
306
+ "all": [
307
+ {
308
+ "serviceTypes": ["admin-ui"]
309
+ },
310
+ {
311
+ "portExposure": "0.0.0.0"
312
+ }
313
+ ]
314
+ },
315
+ "message": "Administrative or UI service is exposed on all interfaces",
316
+ "whyItMatters": "Broad exposure increases the chance of unintended access from other hosts or networks.",
317
+ "recommendation": [
318
+ "Bind local admin services to 127.0.0.1 by default.",
319
+ "Require explicit opt-in for broad network exposure."
320
+ ],
321
+ "enabled": true
322
+ },
323
+ {
324
+ "id": "CDS-SEC-021",
325
+ "title": "Database service published externally",
326
+ "severity": "high",
327
+ "category": "network-exposure",
328
+ "scope": ["service"],
329
+ "match": {
330
+ "all": [
331
+ {
332
+ "serviceTypes": ["database"]
333
+ },
334
+ {
335
+ "portExposure": "host-published"
336
+ }
337
+ ]
338
+ },
339
+ "message": "Database service is published to the host network",
340
+ "whyItMatters": "Direct database exposure significantly expands attack surface.",
341
+ "recommendation": [
342
+ "Avoid publishing database ports unless strictly necessary.",
343
+ "Use internal networking or localhost-only bindings."
344
+ ],
345
+ "enabled": true
346
+ },
347
+ {
348
+ "id": "CDS-SEC-022",
349
+ "title": "Non-local interface exposure in local profile",
350
+ "severity": "medium",
351
+ "category": "network-exposure",
352
+ "scope": ["service"],
353
+ "match": {
354
+ "all": [
355
+ {
356
+ "profileClasses": ["local"]
357
+ },
358
+ {
359
+ "portExposure": "host-published"
360
+ }
361
+ ]
362
+ },
363
+ "message": "A local profile exposes a service beyond localhost",
364
+ "whyItMatters": "Local demo environments are often assumed private but may still be reachable from nearby networks.",
365
+ "recommendation": [
366
+ "Prefer 127.0.0.1 bindings in local profiles.",
367
+ "Require an explicit allow override for broader exposure."
368
+ ],
369
+ "enabled": true
370
+ },
371
+ {
372
+ "id": "CDS-SEC-023",
373
+ "title": "Authenticated service uses plain HTTP",
374
+ "severity": "medium",
375
+ "category": "network-exposure",
376
+ "scope": ["service", "profile-resolved"],
377
+ "match": {
378
+ "any": [
379
+ {
380
+ "pathPatterns": [
381
+ "services.*.url",
382
+ "services.*.ingress.*",
383
+ "services.*.environment.*_URL"
384
+ ],
385
+ "valueRegex": "^http://"
386
+ }
387
+ ]
388
+ },
389
+ "message": "Authenticated or user-facing service appears configured without TLS",
390
+ "whyItMatters": "Plain HTTP can expose credentials or session material over the network.",
391
+ "recommendation": [
392
+ "Use HTTPS/TLS where the service is accessed over a network.",
393
+ "Document exceptions for local-only usage."
394
+ ],
395
+ "enabled": true
396
+ },
397
+ {
398
+ "id": "CDS-SEC-030",
399
+ "$comment": "Deferred: this rule needs an on-disk generated-file scan after render/write. The current matcher only sees profile values and loaded .env secrets, not file contents created by cds render or cds up.",
400
+ "title": "Generated file contains resolved secrets",
401
+ "severity": "high",
402
+ "category": "file-output-safety",
403
+ "scope": ["none"],
404
+ "match": {
405
+ "any": [
406
+ {
407
+ "keyRegex": "(?i)(password|secret|token|api[_-]?key|private[_-]?key|dsn)"
408
+ },
409
+ {
410
+ "entropy": "high",
411
+ "minLength": 16
412
+ }
413
+ ]
414
+ },
415
+ "message": "Generated file appears to contain secret material",
416
+ "whyItMatters": "Files written to disk are easy to copy, commit, back up, or read later by unintended users.",
417
+ "recommendation": [
418
+ "Redact generated output or write secret references only.",
419
+ "Keep rendered artifacts free of resolved secrets."
420
+ ],
421
+ "enabled": false
422
+ },
423
+ {
424
+ "id": "CDS-SEC-031",
425
+ "$comment": "Only the project-root .env is covered by this repo's `/.env` .gitignore entry (leading slash pins it to the root). Any nested .env (profile-scoped, rendered/generated output, or otherwise) is NOT covered by that ignore rule and can be committed accidentally, so the pathPatterns below only flag paths with at least one directory component before the filename; a bare root '.env' is the expected, already-ignored location and must not fire here.",
426
+ "title": "Env file path appears repository-managed or unmanaged",
427
+ "severity": "medium",
428
+ "category": "file-output-safety",
429
+ "scope": ["service-env"],
430
+ "match": {
431
+ "any": [
432
+ {
433
+ "pathPatterns": [
434
+ "*/*.env"
435
+ ]
436
+ }
437
+ ]
438
+ },
439
+ "message": "Environment file path may be stored or handled insecurely",
440
+ "whyItMatters": "Env files often accumulate sensitive data and are easily committed accidentally. This repository's .gitignore only covers the project-root .env; nested .env files (e.g. per-profile or generated output) are not automatically ignored.",
441
+ "recommendation": [
442
+ "Keep secret env files out of generated output directories and version control.",
443
+ "Prefer secret injection over checked-in env files.",
444
+ "If a nested .env location is intentional, add it to .gitignore explicitly."
445
+ ],
446
+ "enabled": true
447
+ },
448
+ {
449
+ "id": "CDS-SEC-032",
450
+ "$comment": "Deferred: the rule schema exposes a permissions condition, but cli/security.py does not evaluate it and the scanner does not stat files on disk. Supporting this needs file metadata collection plus condition evaluation for actual mode bits.",
451
+ "title": "Secret file permissions too broad",
452
+ "severity": "medium",
453
+ "category": "file-output-safety",
454
+ "scope": ["none"],
455
+ "match": {
456
+ "any": [
457
+ {
458
+ "permissions": "world-readable"
459
+ },
460
+ {
461
+ "permissions": "group-readable"
462
+ }
463
+ ]
464
+ },
465
+ "message": "Secret-bearing file permissions are broader than owner-only",
466
+ "whyItMatters": "Local users or processes may read secrets that should be restricted.",
467
+ "recommendation": [
468
+ "Set permissions to owner-only for files containing secrets.",
469
+ "Tighten file creation umask for generated secret artifacts."
470
+ ],
471
+ "enabled": true
472
+ },
473
+ {
474
+ "id": "CDS-SEC-033",
475
+ "title": "Literal secret stored in profile instead of secret source",
476
+ "severity": "medium",
477
+ "category": "file-output-safety",
478
+ "scope": ["profile-raw", "profile-resolved"],
479
+ "match": {
480
+ "all": [
481
+ {
482
+ "keyRegex": "(?i)(password|secret|token|key)"
483
+ },
484
+ {
485
+ "notValueRegex": "^\\$\\{[A-Z0-9_:-]+\\}$"
486
+ }
487
+ ]
488
+ },
489
+ "message": "A secret-like field contains a literal value rather than an external reference",
490
+ "whyItMatters": "Profiles are commonly shared, rendered, or checked into source control.",
491
+ "recommendation": [
492
+ "Use environment or secret-backend references for sensitive fields.",
493
+ "Keep profiles declarative and secret-free."
494
+ ],
495
+ "enabled": true
496
+ },
497
+ {
498
+ "id": "CDS-SEC-040",
499
+ "title": "Unresolved env var falls back to insecure default",
500
+ "severity": "high",
501
+ "category": "config-integrity",
502
+ "scope": ["profile-raw", "profile-resolved", "service-env"],
503
+ "match": {
504
+ "any": [
505
+ {
506
+ "fallbackPattern": "^\\$\\{[A-Z0-9_]+:-?(admin|password|postgres|changeme|secret|default)\\}$"
507
+ }
508
+ ]
509
+ },
510
+ "message": "Environment interpolation uses an insecure fallback default",
511
+ "whyItMatters": "Fallbacks silently weaken security when required env vars are missing.",
512
+ "recommendation": [
513
+ "Remove insecure defaults for secrets.",
514
+ "Fail validation when mandatory secret env vars are absent."
515
+ ],
516
+ "enabled": true
517
+ },
518
+ {
519
+ "id": "CDS-SEC-041",
520
+ "title": "Required env var resolves to empty secret",
521
+ "severity": "high",
522
+ "category": "config-integrity",
523
+ "scope": ["profile-resolved", "service-env"],
524
+ "match": {
525
+ "all": [
526
+ {
527
+ "keyRegex": "(?i)(password|secret|token|key)"
528
+ },
529
+ {
530
+ "allowEmpty": true
531
+ }
532
+ ]
533
+ },
534
+ "message": "Required secret-like field resolves to an empty value",
535
+ "whyItMatters": "Empty secrets may disable protection or trigger insecure application behavior.",
536
+ "recommendation": [
537
+ "Ensure required env vars are present before rendering.",
538
+ "Treat empty secret values as validation failures."
539
+ ],
540
+ "enabled": true
541
+ },
542
+ {
543
+ "id": "CDS-SEC-042",
544
+ "title": "Binding injects secret into unapproved target",
545
+ "severity": "high",
546
+ "category": "config-integrity",
547
+ "scope": ["bindings", "profile-resolved"],
548
+ "match": {
549
+ "all": [
550
+ {
551
+ "keyRegex": "(?i)(password|secret|token|key)"
552
+ },
553
+ {
554
+ "secretSinkPolicy": "forbidden"
555
+ }
556
+ ]
557
+ },
558
+ "message": "Binding appears to route secret material into a forbidden sink",
559
+ "whyItMatters": "Incorrect binding targets can leak secrets to metadata, arguments, or unrelated services.",
560
+ "recommendation": [
561
+ "Allow secrets only in approved sinks such as environment references or secret mounts.",
562
+ "Add policy checks for binding destinations."
563
+ ],
564
+ "enabled": true
565
+ },
566
+ {
567
+ "id": "CDS-SEC-043",
568
+ "title": "Profile inheritance weakens secure defaults",
569
+ "severity": "medium",
570
+ "category": "config-integrity",
571
+ "scope": ["profile-resolved"],
572
+ "match": {
573
+ "any": [
574
+ {
575
+ "pathPatterns": [
576
+ "services.*.ports.*",
577
+ "services.*.env.*",
578
+ "services.*.runtime.*"
579
+ ]
580
+ }
581
+ ]
582
+ },
583
+ "message": "Derived profile appears to override a secure parent setting with an insecure value",
584
+ "whyItMatters": "Inheritance can unintentionally remove protections when child profiles diverge.",
585
+ "recommendation": [
586
+ "Track origin of inherited settings and flag security regressions.",
587
+ "Require explicit approval for overrides that reduce security."
588
+ ],
589
+ "enabled": false
590
+ },
591
+ {
592
+ "id": "CDS-SEC-050",
593
+ "$comment": "Enforced by cli/image_verification.py against rendered Compose service image fields (services.*.image); the rule engine itself does not flatten compose output, so this entry stays scope-nothing as a documented policy intent.",
594
+ "title": "Container image uses latest tag",
595
+ "severity": "medium",
596
+ "category": "supply-chain",
597
+ "scope": ["none"],
598
+ "match": {
599
+ "any": [
600
+ { "pathPatterns": ["services.*.image"] },
601
+ { "valueRegex": "^[^@]+:latest$" },
602
+ { "notValueRegex": "^local/" }
603
+ ]
604
+ },
605
+ "message": "Container image uses the latest tag",
606
+ "whyItMatters": "Floating tags reduce reproducibility and can introduce unexpected changes.",
607
+ "recommendation": [
608
+ "Pin images to explicit versions.",
609
+ "Prefer immutable digests for critical services."
610
+ ],
611
+ "enabled": true
612
+ },
613
+ {
614
+ "id": "CDS-SEC-051",
615
+ "$comment": "Enforced by cli/image_verification.py against rendered Compose service image entries (services.*.image) when the production policy requires digest pins; kept as a scope-nothing rule-set entry for policy documentation.",
616
+ "title": "Critical image missing digest pin",
617
+ "severity": "medium",
618
+ "category": "supply-chain",
619
+ "scope": ["none"],
620
+ "match": {
621
+ "all": [
622
+ { "serviceTypes": ["admin-ui", "database", "api"] },
623
+ { "pathPatterns": ["services.*.image"] },
624
+ { "notValueRegex": "@sha256:[a-f0-9]{64}" }
625
+ ]
626
+ },
627
+ "message": "Critical service image is not pinned by digest",
628
+ "whyItMatters": "Digest pinning helps ensure integrity and repeatable deployments.",
629
+ "recommendation": [
630
+ "Use image digests for critical services.",
631
+ "Define policy exceptions only when necessary."
632
+ ],
633
+ "enabled": true
634
+ },
635
+ {
636
+ "id": "CDS-SEC-052",
637
+ "$comment": "Enforced by cli/image_verification.py against rendered Compose service image fields using the CDS_TRUSTED_REGISTRIES allowlist; the rule engine itself has no rendered-compose scan source, so this entry stays scope-nothing as policy documentation.",
638
+ "title": "Image registry outside allowlist",
639
+ "severity": "medium",
640
+ "category": "supply-chain",
641
+ "scope": ["none"],
642
+ "match": {
643
+ "all": [
644
+ { "pathPatterns": ["services.*.image"] },
645
+ { "notValueRegex": "^(docker\\.io/|ghcr\\.io/|local/)" }
646
+ ]
647
+ },
648
+ "message": "Image registry does not match the configured allowlist",
649
+ "whyItMatters": "Unreviewed registries increase supply-chain risk and reduce provenance confidence.",
650
+ "recommendation": [
651
+ "Restrict images to trusted registries.",
652
+ "Maintain an explicit allowlist per environment."
653
+ ],
654
+ "enabled": true
655
+ },
656
+ {
657
+ "id": "CDS-SEC-053",
658
+ "$comment": "Deferred: minimum-version policy evaluation only makes sense against actual service image references. Today the scanner does not surface services.*.image values from rendered Compose or module implementations.",
659
+ "title": "Image version below policy minimum",
660
+ "severity": "medium",
661
+ "category": "supply-chain",
662
+ "scope": ["none"],
663
+ "match": {
664
+ "any": [
665
+ {
666
+ "imageTagPolicy": "require-tag"
667
+ }
668
+ ]
669
+ },
670
+ "message": "Image tag may violate minimum supported version policy",
671
+ "whyItMatters": "Older images are more likely to contain known vulnerabilities or unsupported components.",
672
+ "recommendation": [
673
+ "Define minimum supported versions for critical services.",
674
+ "Fail validation when image tags fall below policy."
675
+ ],
676
+ "enabled": true
677
+ },
678
+ {
679
+ "id": "CDS-SEC-054",
680
+ "$comment": "Documented policy intent; enforcement is merged into CDS-SEC-050 by cli/image_verification.py, which flags floating ':latest' tags in every profile class. No separate CDS-SEC-054 finding is emitted.",
681
+ "title": "Unpinned image tag in non-local profile",
682
+ "severity": "medium",
683
+ "category": "supply-chain",
684
+ "scope": ["none"],
685
+ "match": {
686
+ "all": [
687
+ { "profileClasses": ["dev", "staging", "prod"] },
688
+ { "pathPatterns": ["services.*.image"] },
689
+ { "imageTagPolicy": "forbid-latest" }
690
+ ]
691
+ },
692
+ "message": "Service image uses the ':latest' tag in a non-local profile",
693
+ "whyItMatters": "Unpinned images can silently introduce breaking changes or vulnerabilities.",
694
+ "recommendation": [
695
+ "Pin images to a specific version tag or digest.",
696
+ "Use digest pinning (@sha256:...) for production environments."
697
+ ],
698
+ "enabled": true
699
+ },
700
+ {
701
+ "id": "CDS-SEC-060",
702
+ "title": "Container runs as root",
703
+ "severity": "medium",
704
+ "category": "runtime-hardening",
705
+ "scope": ["runtime", "service"],
706
+ "match": {
707
+ "any": [
708
+ {
709
+ "pathPatterns": [
710
+ "services.*.user"
711
+ ],
712
+ "equalsAny": ["root", "0"]
713
+ }
714
+ ]
715
+ },
716
+ "message": "Container appears configured to run as root",
717
+ "whyItMatters": "Running as root increases impact if the container is compromised.",
718
+ "recommendation": [
719
+ "Run services as a non-root user where supported.",
720
+ "Define user IDs explicitly in hardened profiles."
721
+ ],
722
+ "enabled": false
723
+ },
724
+ {
725
+ "id": "CDS-SEC-061",
726
+ "title": "Privileged container or excessive capabilities",
727
+ "severity": "high",
728
+ "category": "runtime-hardening",
729
+ "scope": ["runtime", "service"],
730
+ "match": {
731
+ "any": [
732
+ {
733
+ "pathPatterns": [
734
+ "services.*.privileged"
735
+ ],
736
+ "equalsAny": ["true", "True", "1"]
737
+ },
738
+ {
739
+ "runtimeFlags": [
740
+ "CAP_SYS_ADMIN",
741
+ "CAP_NET_ADMIN",
742
+ "privileged"
743
+ ]
744
+ }
745
+ ]
746
+ },
747
+ "message": "Container requests privileged mode or broad kernel capabilities",
748
+ "whyItMatters": "Privileged execution significantly increases host compromise risk.",
749
+ "recommendation": [
750
+ "Disable privileged mode unless strictly required.",
751
+ "Grant the minimum capabilities necessary."
752
+ ],
753
+ "enabled": true
754
+ },
755
+ {
756
+ "id": "CDS-SEC-062",
757
+ "title": "Writable root filesystem",
758
+ "severity": "medium",
759
+ "category": "runtime-hardening",
760
+ "scope": ["runtime", "service"],
761
+ "match": {
762
+ "any": [
763
+ {
764
+ "pathPatterns": [
765
+ "services.*.read_only"
766
+ ],
767
+ "equalsAny": ["false", "False", "0"]
768
+ }
769
+ ]
770
+ },
771
+ "message": "Service does not enforce a read-only root filesystem",
772
+ "whyItMatters": "Writable roots make persistence and tampering easier after compromise.",
773
+ "recommendation": [
774
+ "Enable read-only root filesystems where feasible.",
775
+ "Use explicit writable mounts for required paths."
776
+ ],
777
+ "enabled": false
778
+ },
779
+ {
780
+ "id": "CDS-SEC-063",
781
+ "title": "Sensitive host path mount",
782
+ "severity": "high",
783
+ "category": "runtime-hardening",
784
+ "scope": ["runtime", "service"],
785
+ "match": {
786
+ "any": [
787
+ {
788
+ "pathPatterns": [
789
+ "services.*.volumes.*"
790
+ ],
791
+ "containsAny": [
792
+ "/var/run/docker.sock",
793
+ "/:/",
794
+ "/root",
795
+ "/etc/ssh",
796
+ "/etc/passwd",
797
+ "/etc/shadow"
798
+ ]
799
+ }
800
+ ]
801
+ },
802
+ "message": "Service mounts a sensitive host path",
803
+ "whyItMatters": "Sensitive host mounts can expose control of the host or critical credentials.",
804
+ "recommendation": [
805
+ "Remove sensitive host mounts unless essential.",
806
+ "Use narrower read-only mounts and isolate access."
807
+ ],
808
+ "enabled": true
809
+ },
810
+ {
811
+ "id": "CDS-SEC-070",
812
+ "$comment": "Evaluated against the rendered Compose services.*.command/entrypoint/healthcheck/logging fields (the \"rendered-compose\" scope), not raw profile/env inputs -- see _flatten_rendered_leak_surfaces in cli/security.py. This is the check that would have caught the Vault dev-root-token-as-command-arg issue fixed in PR #290. Only the \"${CDS_\" arm below can ever fire against rendered output: keyRegex only tests the final path segment (cli/security.py's path.split(\".\")[-1]), which for rendered command/entrypoint/healthcheck items is a list index like \"command[2]\" and for logging is an ordinary leaf key like \"driver\" -- never a password/secret/token/key-shaped name. And \"${secrets.*}\" is always resolved to \"${CDS_*}\" by render time, while an unresolved \"${config.*}\" raises E071 and stops the render (see _try_render_compose_for_scan), so that alternative can never appear in a successfully rendered document either. Scope note: this rule only catches unresolved ${CDS_*} placeholders leaking into these fields, not literal hardcoded secrets (e.g. \"--token=hunter2\") -- rendered-value entropy/keyword scanning was dropped from the match set above because it never actually fired here (see history), so literal secrets in command/entrypoint/healthcheck/logging remain out of scope for this rule; CDS-SEC-013 (secret reuse) and the profile-input secret rules cover literal-secret cases upstream instead.",
813
+ "title": "Secret appears in command args or log configuration",
814
+ "severity": "high",
815
+ "category": "observability-leakage",
816
+ "scope": ["rendered-compose"],
817
+ "match": {
818
+ "any": [
819
+ {
820
+ "pathPatterns": [
821
+ "services.*.command*",
822
+ "services.*.entrypoint*",
823
+ "services.*.healthcheck*",
824
+ "services.*.logging*"
825
+ ],
826
+ "valueRegex": "\\$\\{CDS_"
827
+ }
828
+ ]
829
+ },
830
+ "message": "Secret-like material appears in command-line arguments or logging config",
831
+ "whyItMatters": "Arguments and logging settings are commonly exposed through process listings and diagnostics.",
832
+ "recommendation": [
833
+ "Pass secrets through environment references or mounted secret files.",
834
+ "Avoid including secret values in command-line flags."
835
+ ],
836
+ "enabled": true
837
+ },
838
+ {
839
+ "id": "CDS-SEC-071",
840
+ "$comment": "Deferred: this rule is about literal CLI plan/validate output text, which is produced after formatting findings/diagnostics. The current matcher has no capture of stdout/stderr or rendered plan text to scan.",
841
+ "title": "CLI plan or validate output prints secrets",
842
+ "severity": "high",
843
+ "category": "observability-leakage",
844
+ "scope": ["none"],
845
+ "match": {
846
+ "any": [
847
+ {
848
+ "keyRegex": "(?i)(password|secret|token|api[_-]?key|private[_-]?key|dsn)"
849
+ },
850
+ {
851
+ "valueRegex": "-----BEGIN (RSA )?PRIVATE KEY-----"
852
+ },
853
+ {
854
+ "entropy": "high",
855
+ "minLength": 16
856
+ }
857
+ ]
858
+ },
859
+ "message": "CLI output appears to contain sensitive values",
860
+ "whyItMatters": "Terminal output is often copied into tickets, CI logs, or chat systems.",
861
+ "recommendation": [
862
+ "Redact sensitive output by default.",
863
+ "Provide explicit unsafe flags for debugging only."
864
+ ],
865
+ "enabled": false
866
+ },
867
+ {
868
+ "id": "CDS-SEC-072",
869
+ "title": "Debug mode enabled for sensitive service",
870
+ "severity": "medium",
871
+ "category": "observability-leakage",
872
+ "scope": ["service", "service-env"],
873
+ "match": {
874
+ "any": [
875
+ {
876
+ "pathPatterns": [
877
+ "services.*.environment.DEBUG",
878
+ "services.*.environment.FLASK_DEBUG",
879
+ "services.*.logging.level"
880
+ ],
881
+ "equalsAny": ["true", "True", "1", "debug", "DEBUG"]
882
+ }
883
+ ]
884
+ },
885
+ "message": "Debug or overly verbose logging appears enabled",
886
+ "whyItMatters": "Debug modes frequently expose configuration values, stack traces, or credentials.",
887
+ "recommendation": [
888
+ "Disable debug logging in shared or sensitive environments.",
889
+ "Use controlled debug overrides only when necessary."
890
+ ],
891
+ "enabled": true
892
+ },
893
+ {
894
+ "id": "CDS-SEC-073",
895
+ "title": "Non-production-suitable module used in a staging/production profile",
896
+ "severity": "high",
897
+ "category": "config-integrity",
898
+ "scope": ["module-values"],
899
+ "match": {
900
+ "all": [
901
+ {
902
+ "pathPatterns": ["_module.productionSuitable"],
903
+ "equalsAny": ["False"]
904
+ },
905
+ {
906
+ "profileClasses": ["staging", "prod"]
907
+ }
908
+ ]
909
+ },
910
+ "message": "Profile declares a staging/production environment but uses a module marked productionSuitable: false",
911
+ "whyItMatters": "A module's own metadata (e.g. a dev-mode server with no unsealing or hardcoded credentials) says it is not meant for production. A profile claiming production support while depending on it is a contradictory declaration.",
912
+ "recommendation": [
913
+ "Use a production-suitable implementation of this module for staging/production profiles.",
914
+ "If no production-suitable implementation exists yet, declare this profile's environment as local or development instead."
915
+ ],
916
+ "enabled": true
917
+ }
918
+ ]
919
+ }