composable-data-stack 0.9.1__tar.gz → 0.10.0__tar.gz
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.
- {composable_data_stack-0.9.1/composable_data_stack.egg-info → composable_data_stack-0.10.0}/PKG-INFO +4 -2
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/README.md +2 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/image_verification.py +12 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/k8s_security.py +13 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/main.py +136 -12
- composable_data_stack-0.10.0/cli/resources/compatibility-registry.json +31 -0
- composable_data_stack-0.10.0/cli/resources/compatibility-registry.schema.json +70 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/resources/profile.schema.json +28 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/resources/rule-schema.json +24 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/resources/rule-set.json +55 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/security.py +317 -18
- composable_data_stack-0.10.0/cli/security_common.py +101 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/validator.py +108 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0/composable_data_stack.egg-info}/PKG-INFO +4 -2
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/composable_data_stack.egg-info/SOURCES.txt +7 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/composable_data_stack.egg-info/requires.txt +1 -1
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/pyproject.toml +2 -2
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_cds_workflow.py +85 -0
- composable_data_stack-0.10.0/tests/test_check_roadmap_freshness.py +203 -0
- composable_data_stack-0.10.0/tests/test_generate_release_inventory.py +197 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_image_verification.py +13 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_k8s_security.py +28 -0
- composable_data_stack-0.10.0/tests/test_keycloak_postgres_wiring.py +169 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_main.py +225 -1
- composable_data_stack-0.10.0/tests/test_release_notes_support.py +79 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_security.py +452 -1
- composable_data_stack-0.10.0/tests/test_validate_cli_sbom.py +145 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_validator.py +172 -1
- composable_data_stack-0.9.1/cli/security_common.py +0 -40
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/LICENSE +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/__init__.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/constants.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/diagnostics.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/getter.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/graph.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/image_updates.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/k8s_renderer.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/k8s_runner.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/loader.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/overlay.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/planner.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/preflight.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/renderer.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/resolver.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/resources/__init__.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/resources/contract.schema.json +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/resources/module.schema.json +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/secrets.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/state.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/up_runner.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/utils.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/composable_data_stack.egg-info/dependency_links.txt +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/composable_data_stack.egg-info/entry_points.txt +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/composable_data_stack.egg-info/top_level.txt +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/setup.cfg +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_ai_profile_review.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_bump_changelog.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_completion.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_compose_runtime_smoke.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_compose_to_module.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_dagster_config_generation.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_dagster_definitions_uri.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_dagster_entrypoint_backend_guard.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_dagster_hardening.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_dbt_hardening.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_dlt_postgres_wiring.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_duckdb_hardening.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_environment_classification.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_environment_cli.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_getter.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_graph.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_image_security_scan_workflow.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_image_updates.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_k3d_e2e_profile.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_k3d_exposure.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_k8s_renderer.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_k8s_runner.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_loader.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_markdown_mermaid_blocks.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_module_isolation.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_modules_no_committed_secrets.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_mvp_proof_workflow.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_overlay.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_planner.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_preflight.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_publish_images_workflow.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_python_version_alignment.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_release_version_check.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_render_example_profile.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_renderer.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_resolve_profile_path.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_resolver.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_secrets.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_smoke_example_profile.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_standalone_module_profile.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_state.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_superset_hardening.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_tender_analytics.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_tender_dashboard.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_test_runner_timeout.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_trivyignore.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_up_runner.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_upstream_image_hardening.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_utils.py +0 -0
- {composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/tests/test_vendored_llm_client.py +0 -0
{composable_data_stack-0.9.1/composable_data_stack.egg-info → composable_data_stack-0.10.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: composable-data-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: CLI for validating and composing composable data stack profiles
|
|
5
5
|
Author: Ronald Hensbergen
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -32,7 +32,7 @@ Provides-Extra: dev
|
|
|
32
32
|
Requires-Dist: coverage>=7.6; extra == "dev"
|
|
33
33
|
Requires-Dist: bandit>=1.7; extra == "dev"
|
|
34
34
|
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
35
|
-
Requires-Dist: ruff==0.16.
|
|
35
|
+
Requires-Dist: ruff==0.16.9; extra == "dev"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
38
38
|
# 🚀 Composable Data Stack (CDS)
|
|
@@ -892,6 +892,7 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
|
|
|
892
892
|
| `[E041] ... Contract ref "x.y" points to unknown module "x"` | A `consumes` binding's `contractRef` refers to a module ID that isn't defined in the profile. | Check `spec.modules` for the correct module `id`, and confirm the contract ref follows `<module-id>.<contract-name>`. |
|
|
893
893
|
| `[E041] ... but it does not provide "<contract-name>"` | The referenced module exists, but its `spec.provides` list doesn't expose that contract name. | Check the producing module's `module.yaml` for the contracts it actually provides, and fix the consumer's `contractRef` to match. |
|
|
894
894
|
| `[E042] ... Contract kind mismatch` | The consumer expects one contract kind (e.g. `sql-database`) but the producer exposes a different kind. | Point the binding at a module that provides the expected contract kind, or update the consumer's expected kind if the mismatch is intentional. |
|
|
895
|
+
| `[E043] ... recorded as unsupported in the compatibility registry` | The binding's `kind` matches, but this exact provider/consumer pairing is explicitly recorded as `unsupported` in `cli/resources/compatibility-registry.json` -- it's known NOT to work together despite matching shape. | Use a different provider/consumer pairing for this contract, or if the pairing has since been fixed/verified, update its `status` to `tested` in the registry (see [docs/architecture.md § Secrets and contract resolution](docs/architecture.md#secrets-and-contract-resolution)). |
|
|
895
896
|
| `[E103] ... config.image.tag is required ... when config.image.source is "registry"` | A module instance sets `config.image.source: registry` without also setting `config.image.tag`. | Set `config.image.tag` to a version, or switch back to `config.image.source: build`. |
|
|
896
897
|
| `[W097] ... config.image.tag is "latest" with config.image.source "registry"` | `config.image.tag: latest` under `source: registry` still validates, but drifts silently between deploys instead of pinning a reproducible version. | Pin an explicit tag from `publish-images.yml`'s output (or `tests/fixtures/signed-images.json`) instead of `latest`. |
|
|
897
898
|
|
|
@@ -1036,6 +1037,7 @@ Please read these first:
|
|
|
1036
1037
|
- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
1037
1038
|
- [SECURITY.md](SECURITY.md)
|
|
1038
1039
|
- [SUPPORT.md](SUPPORT.md)
|
|
1040
|
+
- [docs/security-support-policy.md](docs/security-support-policy.md)
|
|
1039
1041
|
- [CHANGELOG.md](CHANGELOG.md)
|
|
1040
1042
|
- [RELEASE.md](RELEASE.md)
|
|
1041
1043
|
|
|
@@ -855,6 +855,7 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
|
|
|
855
855
|
| `[E041] ... Contract ref "x.y" points to unknown module "x"` | A `consumes` binding's `contractRef` refers to a module ID that isn't defined in the profile. | Check `spec.modules` for the correct module `id`, and confirm the contract ref follows `<module-id>.<contract-name>`. |
|
|
856
856
|
| `[E041] ... but it does not provide "<contract-name>"` | The referenced module exists, but its `spec.provides` list doesn't expose that contract name. | Check the producing module's `module.yaml` for the contracts it actually provides, and fix the consumer's `contractRef` to match. |
|
|
857
857
|
| `[E042] ... Contract kind mismatch` | The consumer expects one contract kind (e.g. `sql-database`) but the producer exposes a different kind. | Point the binding at a module that provides the expected contract kind, or update the consumer's expected kind if the mismatch is intentional. |
|
|
858
|
+
| `[E043] ... recorded as unsupported in the compatibility registry` | The binding's `kind` matches, but this exact provider/consumer pairing is explicitly recorded as `unsupported` in `cli/resources/compatibility-registry.json` -- it's known NOT to work together despite matching shape. | Use a different provider/consumer pairing for this contract, or if the pairing has since been fixed/verified, update its `status` to `tested` in the registry (see [docs/architecture.md § Secrets and contract resolution](docs/architecture.md#secrets-and-contract-resolution)). |
|
|
858
859
|
| `[E103] ... config.image.tag is required ... when config.image.source is "registry"` | A module instance sets `config.image.source: registry` without also setting `config.image.tag`. | Set `config.image.tag` to a version, or switch back to `config.image.source: build`. |
|
|
859
860
|
| `[W097] ... config.image.tag is "latest" with config.image.source "registry"` | `config.image.tag: latest` under `source: registry` still validates, but drifts silently between deploys instead of pinning a reproducible version. | Pin an explicit tag from `publish-images.yml`'s output (or `tests/fixtures/signed-images.json`) instead of `latest`. |
|
|
860
861
|
|
|
@@ -999,6 +1000,7 @@ Please read these first:
|
|
|
999
1000
|
- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
|
|
1000
1001
|
- [SECURITY.md](SECURITY.md)
|
|
1001
1002
|
- [SUPPORT.md](SUPPORT.md)
|
|
1003
|
+
- [docs/security-support-policy.md](docs/security-support-policy.md)
|
|
1002
1004
|
- [CHANGELOG.md](CHANGELOG.md)
|
|
1003
1005
|
- [RELEASE.md](RELEASE.md)
|
|
1004
1006
|
|
|
@@ -156,6 +156,18 @@ def _finding(
|
|
|
156
156
|
return {
|
|
157
157
|
"rule_id": rule_id,
|
|
158
158
|
"severity": severity,
|
|
159
|
+
# These findings aren't declared in cli/resources/rule-set.json (see
|
|
160
|
+
# docs/image-signing.md for why), so their compliance category can't
|
|
161
|
+
# be looked up the way cli.security.run_security_validation() does
|
|
162
|
+
# for rule-set findings. Every check here is about image tag/digest
|
|
163
|
+
# pinning, registry trust, or signature/provenance verification --
|
|
164
|
+
# i.e. keeping deployed images current, authentic, and traceable to
|
|
165
|
+
# a trusted build -- which is exactly the use case
|
|
166
|
+
# docs/security-compliance-categories.md documents "patching" for,
|
|
167
|
+
# so tag them directly with that
|
|
168
|
+
# cli.security_common.COMPLIANCE_CATEGORIES value instead of
|
|
169
|
+
# leaving them uncategorized.
|
|
170
|
+
"category": "patching",
|
|
159
171
|
"module": service,
|
|
160
172
|
"message": message,
|
|
161
173
|
"path": f"services.{service}.image",
|
|
@@ -76,6 +76,7 @@ def scan_k8s_security(plan: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
76
76
|
f"{path}.resources.{container_name}",
|
|
77
77
|
module_id,
|
|
78
78
|
["Declare both requests and limits for the container."],
|
|
79
|
+
category="configuration-management",
|
|
79
80
|
))
|
|
80
81
|
return findings
|
|
81
82
|
|
|
@@ -113,10 +114,22 @@ def _finding(
|
|
|
113
114
|
path: str,
|
|
114
115
|
module: str,
|
|
115
116
|
recommendation: list[str],
|
|
117
|
+
*,
|
|
118
|
+
category: str = "system-hardening",
|
|
116
119
|
) -> dict[str, Any]:
|
|
117
120
|
return {
|
|
118
121
|
"rule_id": rule_id,
|
|
119
122
|
"severity": severity,
|
|
123
|
+
# These findings aren't declared in cli/resources/rule-set.json (the
|
|
124
|
+
# declarative rule-set engine has no scope for the plan's
|
|
125
|
+
# Kubernetes-specific implementation), so their compliance category
|
|
126
|
+
# can't be looked up the way cli.security.run_security_validation()
|
|
127
|
+
# does for rule-set findings. CDS-K8S-001-004 are container/pod
|
|
128
|
+
# runtime-hardening posture checks, so they default to the matching
|
|
129
|
+
# cli.security_common.COMPLIANCE_CATEGORIES value; CDS-K8S-005
|
|
130
|
+
# (resource requests/limits) overrides this below since it's a
|
|
131
|
+
# capacity-governance concern rather than runtime-hardening posture.
|
|
132
|
+
"category": category,
|
|
120
133
|
"message": message,
|
|
121
134
|
"path": path,
|
|
122
135
|
"module": module,
|
|
@@ -38,7 +38,7 @@ from .planner import build_plan
|
|
|
38
38
|
from .preflight import preflight_passed, run_preflight
|
|
39
39
|
from .renderer import render_compose
|
|
40
40
|
from .security import PrecomputedRender, run_security_validation
|
|
41
|
-
from .security_common import SEVERITY_ORDER, infer_profile_class
|
|
41
|
+
from .security_common import COMPLIANCE_CATEGORIES, SEVERITY_ORDER, infer_profile_class
|
|
42
42
|
from .state import format_state_output, group_services_by_health, parse_compose_ps_json
|
|
43
43
|
from .up_runner import (
|
|
44
44
|
DEFAULT_TIMEOUT_SECONDS,
|
|
@@ -875,6 +875,58 @@ def _unverifiable_image_finding(message: str) -> dict[str, Any]:
|
|
|
875
875
|
}
|
|
876
876
|
|
|
877
877
|
|
|
878
|
+
_UNCATEGORIZED_LABEL = "uncategorized"
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def _filter_findings_by_category(
|
|
882
|
+
findings: list[dict[str, Any]], categories: list[str] | None
|
|
883
|
+
) -> list[dict[str, Any]]:
|
|
884
|
+
"""
|
|
885
|
+
Restrict findings to the given compliance control categories.
|
|
886
|
+
|
|
887
|
+
Every finding source (the declarative rule-set engine, scan_k8s_security,
|
|
888
|
+
image verification) tags its findings with a "category" at generation
|
|
889
|
+
time (see cli.security.run_security_validation(), cli.k8s_security,
|
|
890
|
+
cli.image_verification), so this is a uniform key lookup; there is no
|
|
891
|
+
remaining source of genuinely uncategorized findings today, but a
|
|
892
|
+
missing "category" is still treated as excluded rather than raising, in
|
|
893
|
+
case a future finding source doesn't set one.
|
|
894
|
+
"""
|
|
895
|
+
if not categories:
|
|
896
|
+
return findings
|
|
897
|
+
wanted = set(categories)
|
|
898
|
+
return [f for f in findings if f.get("category") in wanted]
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _group_findings_by_category(
|
|
902
|
+
findings: list[dict[str, Any]],
|
|
903
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
904
|
+
"""
|
|
905
|
+
Group findings by their compliance control category, preserving each
|
|
906
|
+
group's existing (severity/rule_id/...) ordering. A finding without a
|
|
907
|
+
"category" is grouped under "uncategorized" rather than dropped, as a
|
|
908
|
+
defensive fallback (see _filter_findings_by_category()); in practice
|
|
909
|
+
every current finding source sets one.
|
|
910
|
+
"""
|
|
911
|
+
grouped: dict[str, list[dict[str, Any]]] = {}
|
|
912
|
+
for finding in findings:
|
|
913
|
+
grouped.setdefault(finding.get("category") or _UNCATEGORIZED_LABEL, []).append(finding)
|
|
914
|
+
return dict(sorted(grouped.items()))
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _category_heading(category: str) -> str:
|
|
918
|
+
"""
|
|
919
|
+
Render a "== <label> (<category>) ==" heading for --group-by-category,
|
|
920
|
+
falling back to just "== <category> ==" for the uncategorized bucket,
|
|
921
|
+
which has no COMPLIANCE_CATEGORIES entry.
|
|
922
|
+
"""
|
|
923
|
+
entry = COMPLIANCE_CATEGORIES.get(category)
|
|
924
|
+
if entry is None:
|
|
925
|
+
return f"== {category} =="
|
|
926
|
+
label, _rationale = entry
|
|
927
|
+
return f"== {label} ({category}) =="
|
|
928
|
+
|
|
929
|
+
|
|
878
930
|
def _run_image_verification(
|
|
879
931
|
profile_path: str,
|
|
880
932
|
environment: str | None,
|
|
@@ -1159,6 +1211,21 @@ def main() -> int:
|
|
|
1159
1211
|
"By default, values are redacted to avoid echoing real secrets to stdout/CI logs."
|
|
1160
1212
|
),
|
|
1161
1213
|
)
|
|
1214
|
+
test_parser.add_argument(
|
|
1215
|
+
"--category",
|
|
1216
|
+
action="append",
|
|
1217
|
+
choices=COMPLIANCE_CATEGORIES,
|
|
1218
|
+
help=(
|
|
1219
|
+
"Only print security findings tagged with this compliance control "
|
|
1220
|
+
"category (repeatable); does not affect the security stage's "
|
|
1221
|
+
"pass/fail outcome. See `cds security --help` for details."
|
|
1222
|
+
),
|
|
1223
|
+
)
|
|
1224
|
+
test_parser.add_argument(
|
|
1225
|
+
"--group-by-category",
|
|
1226
|
+
action="store_true",
|
|
1227
|
+
help="Print security findings grouped by compliance control category instead of one flat list.",
|
|
1228
|
+
)
|
|
1162
1229
|
_add_hardened_arg(test_parser)
|
|
1163
1230
|
_add_image_source_arg(test_parser)
|
|
1164
1231
|
|
|
@@ -1283,6 +1350,25 @@ def main() -> int:
|
|
|
1283
1350
|
"tests/fixtures/signed-images.json) for offline verification."
|
|
1284
1351
|
),
|
|
1285
1352
|
)
|
|
1353
|
+
security_parser.add_argument(
|
|
1354
|
+
"--category",
|
|
1355
|
+
action="append",
|
|
1356
|
+
choices=COMPLIANCE_CATEGORIES,
|
|
1357
|
+
help=(
|
|
1358
|
+
"Only print findings tagged with this compliance control category "
|
|
1359
|
+
"(repeatable); does not change which rules run or the command's "
|
|
1360
|
+
"exit code -- a high-severity finding outside the requested "
|
|
1361
|
+
"category still fails the scan. Categories are an informational "
|
|
1362
|
+
"readiness aid for organizing findings against a user's own risk "
|
|
1363
|
+
"assessment (e.g. NIS2/Cyberbeveiligingswet), not a compliance "
|
|
1364
|
+
"certification -- see docs/security-compliance-categories.md."
|
|
1365
|
+
),
|
|
1366
|
+
)
|
|
1367
|
+
security_parser.add_argument(
|
|
1368
|
+
"--group-by-category",
|
|
1369
|
+
action="store_true",
|
|
1370
|
+
help="Print findings grouped by compliance control category instead of one flat list.",
|
|
1371
|
+
)
|
|
1286
1372
|
|
|
1287
1373
|
diff_parser = subparsers.add_parser(
|
|
1288
1374
|
"diff",
|
|
@@ -1941,14 +2027,29 @@ def main() -> int:
|
|
|
1941
2027
|
))
|
|
1942
2028
|
for diag in sec_diags:
|
|
1943
2029
|
print(diag.format(), file=sys.stderr)
|
|
1944
|
-
|
|
1945
|
-
|
|
2030
|
+
displayed_findings = _filter_findings_by_category(findings, args.category)
|
|
2031
|
+
if args.group_by_category:
|
|
2032
|
+
for category, group in _group_findings_by_category(displayed_findings).items():
|
|
2033
|
+
print(_category_heading(category))
|
|
2034
|
+
for f in group:
|
|
2035
|
+
print(f"[{f['severity'].upper()}] {f['rule_id']} {f['message']}")
|
|
2036
|
+
else:
|
|
2037
|
+
for f in displayed_findings:
|
|
2038
|
+
print(f"[{f['severity'].upper()}] {f['rule_id']} {f['message']}")
|
|
1946
2039
|
# A W096 warning means rendered-compose-scoped rules (e.g.
|
|
1947
2040
|
# CDS-SEC-070) were silently skipped due to an unexpected
|
|
1948
2041
|
# error during rendering. Treat that the same as a failed
|
|
1949
2042
|
# security stage rather than letting any non-high finding
|
|
1950
2043
|
# (or no finding at all) mask the fact that some checks
|
|
1951
2044
|
# never ran (GHSA-mx5p-cv63-6829).
|
|
2045
|
+
#
|
|
2046
|
+
# security_ok is intentionally computed from the full,
|
|
2047
|
+
# unfiltered `findings`, not `displayed_findings`: --category
|
|
2048
|
+
# is a display-only filter (see docs/security-compliance-
|
|
2049
|
+
# categories.md), so narrowing what's printed must never
|
|
2050
|
+
# narrow what counts as a failing scan -- that would let a
|
|
2051
|
+
# real high-severity finding outside the requested category
|
|
2052
|
+
# silently report success.
|
|
1952
2053
|
render_scan_skipped = any(d.code == "W096" for d in sec_diags)
|
|
1953
2054
|
security_ok = (
|
|
1954
2055
|
not render_scan_skipped
|
|
@@ -2293,15 +2394,38 @@ def main() -> int:
|
|
|
2293
2394
|
print("No security findings.")
|
|
2294
2395
|
return 0
|
|
2295
2396
|
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2397
|
+
# --category is a display-only filter (see docs/security-compliance-
|
|
2398
|
+
# categories.md): it must never narrow what counts as a failing scan,
|
|
2399
|
+
# only what gets printed. The exit code below is deliberately
|
|
2400
|
+
# computed from the full, unfiltered `findings`, not
|
|
2401
|
+
# `displayed_findings` -- otherwise a real high-severity finding
|
|
2402
|
+
# outside the requested category would silently report success.
|
|
2403
|
+
displayed_findings = _filter_findings_by_category(findings, args.category)
|
|
2404
|
+
|
|
2405
|
+
if not displayed_findings:
|
|
2406
|
+
print(f"No security findings in category: {', '.join(sorted(set(args.category)))}.")
|
|
2407
|
+
elif args.group_by_category:
|
|
2408
|
+
for category, group in _group_findings_by_category(displayed_findings).items():
|
|
2409
|
+
print(_category_heading(category))
|
|
2410
|
+
for f in group:
|
|
2411
|
+
print(f"[{f['severity'].upper()}] {f['rule_id']} {f['message']}")
|
|
2412
|
+
print(f" object: {f['path']}")
|
|
2413
|
+
print(f" module: {f['module']}")
|
|
2414
|
+
if f["value"] is not None:
|
|
2415
|
+
print(f" value: {f['value']}")
|
|
2416
|
+
for rec in f["recommendation"]:
|
|
2417
|
+
print(f" fix: {rec}")
|
|
2418
|
+
print()
|
|
2419
|
+
else:
|
|
2420
|
+
for f in displayed_findings:
|
|
2421
|
+
print(f"[{f['severity'].upper()}] {f['rule_id']} {f['message']}")
|
|
2422
|
+
print(f" object: {f['path']}")
|
|
2423
|
+
print(f" module: {f['module']}")
|
|
2424
|
+
if f["value"] is not None:
|
|
2425
|
+
print(f" value: {f['value']}")
|
|
2426
|
+
for rec in f["recommendation"]:
|
|
2427
|
+
print(f" fix: {rec}")
|
|
2428
|
+
print()
|
|
2305
2429
|
|
|
2306
2430
|
# As above: a W096 warning means some rendered-compose-scoped rules
|
|
2307
2431
|
# were skipped, so even when only non-high findings are present the
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "compatibility-registry.schema.json",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"metadata": {
|
|
5
|
+
"name": "cds-core-compatibility-registry",
|
|
6
|
+
"description": "Central record of module pairings known to have been tested together (\"tested\"), or known NOT to work together despite matching a contract's kind (\"unsupported\"). Kept as a single file rather than scattered across individual module.yaml documents so composition knowledge about the whole stack stays in one place (see issue #350). A pairing with no entry here is only structurally compatible (same contract kind); it is neither vouched for nor ruled out."
|
|
7
|
+
},
|
|
8
|
+
"pairings": [
|
|
9
|
+
{
|
|
10
|
+
"contract": "sql-database",
|
|
11
|
+
"provider": "warehouse/postgres",
|
|
12
|
+
"consumer": "orchestration/dagster",
|
|
13
|
+
"status": "tested",
|
|
14
|
+
"notes": "Exercised end-to-end by profiles/local-dagster-postgres-superset/profile.yaml, covered by the \"CDS Test (Stable Profile)\" CI workflow."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"contract": "sql-database",
|
|
18
|
+
"provider": "warehouse/postgres",
|
|
19
|
+
"consumer": "bi/superset",
|
|
20
|
+
"status": "tested",
|
|
21
|
+
"notes": "Exercised end-to-end by profiles/local-dagster-postgres-superset/profile.yaml, covered by the \"CDS Test (Stable Profile)\" CI workflow."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"contract": "cache-service",
|
|
25
|
+
"provider": "cache/keydb",
|
|
26
|
+
"consumer": "bi/superset",
|
|
27
|
+
"status": "tested",
|
|
28
|
+
"notes": "Exercised end-to-end by profiles/local-dagster-postgres-superset/profile.yaml, covered by the \"CDS Test (Stable Profile)\" CI workflow."
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "./compatibility-registry.schema.json",
|
|
4
|
+
"title": "Composable Data Stack Compatibility Registry",
|
|
5
|
+
"description": "Central, non-scattered record of module pairings for a given contract that are known to have been tested together, or explicitly known NOT to work together. This is deliberately separate from plain contract `kind` matching (structural compatibility): an entry here expresses practical, known-good/known-bad compatibility, not just shape compatibility. Absence of an entry for a pairing means 'not yet recorded' -- it is neither tested nor unsupported.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["version", "metadata", "pairings"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"$schema": {
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"version": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"metadata": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"required": ["name"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"name": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1
|
|
24
|
+
},
|
|
25
|
+
"description": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"pairings": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": {
|
|
33
|
+
"$ref": "#/$defs/pairing"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"$defs": {
|
|
38
|
+
"pairing": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"required": ["contract", "provider", "consumer", "status"],
|
|
42
|
+
"properties": {
|
|
43
|
+
"contract": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"pattern": "^[a-z0-9-]+$",
|
|
46
|
+
"description": "Contract kind this pairing applies to (matches a module's provides[].contract.kind / consumes[].contract.kind), not a specific provide-name. A provider can expose several provide-names under the same kind (e.g. warehouse/postgres provides \"sql-database\", \"dagster-database\", and \"superset-database\", all kind \"sql-database\"); one row here is intended to cover the provider/consumer pair for all of them at this kind granularity. A row also carries no module version, so a \"tested\" status remains valid across version changes on either side (tracked as a follow-up, see issue #750)."
|
|
47
|
+
},
|
|
48
|
+
"provider": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"pattern": "^[a-z0-9-]+/[a-z0-9-]+$",
|
|
51
|
+
"description": "Providing module's identity, as \"<metadata.category>/<metadata.name>\" (e.g. \"warehouse/postgres\"). Stable across profiles, unlike a profile-scoped module instance id or a source path."
|
|
52
|
+
},
|
|
53
|
+
"consumer": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"pattern": "^[a-z0-9-]+/[a-z0-9-]+$",
|
|
56
|
+
"description": "Consuming module's identity, as \"<metadata.category>/<metadata.name>\" (e.g. \"orchestration/dagster\")."
|
|
57
|
+
},
|
|
58
|
+
"status": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"enum": ["tested", "unsupported"],
|
|
61
|
+
"description": "\"tested\": this exact provider/consumer pairing for this contract has been exercised together (e.g. by a profile under active CI coverage) and is known-good. \"unsupported\": this pairing is structurally compatible (same contract kind) but has been found NOT to work together in practice; validation reports this as an error."
|
|
62
|
+
},
|
|
63
|
+
"notes": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "Free-text context, e.g. which profile/test exercises a \"tested\" pairing, or why an \"unsupported\" pairing fails."
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
{composable_data_stack-0.9.1 → composable_data_stack-0.10.0}/cli/resources/profile.schema.json
RENAMED
|
@@ -154,6 +154,21 @@
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
+
},
|
|
158
|
+
"security": {
|
|
159
|
+
"type": "object",
|
|
160
|
+
"additionalProperties": false,
|
|
161
|
+
"properties": {
|
|
162
|
+
"waivers": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"additionalProperties": false,
|
|
165
|
+
"properties": {
|
|
166
|
+
"plaintextEndpointExposure": {
|
|
167
|
+
"$ref": "#/$defs/plaintextEndpointExposureWaiver"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
157
172
|
}
|
|
158
173
|
}
|
|
159
174
|
}
|
|
@@ -231,6 +246,19 @@
|
|
|
231
246
|
"default": "bridge"
|
|
232
247
|
}
|
|
233
248
|
}
|
|
249
|
+
},
|
|
250
|
+
"plaintextEndpointExposureWaiver": {
|
|
251
|
+
"type": "object",
|
|
252
|
+
"additionalProperties": false,
|
|
253
|
+
"required": ["reason"],
|
|
254
|
+
"properties": {
|
|
255
|
+
"reason": {
|
|
256
|
+
"type": "string",
|
|
257
|
+
"minLength": 1,
|
|
258
|
+
"pattern": "\\S",
|
|
259
|
+
"description": "Must contain at least one non-whitespace character; cli/security.py strips and treats an all-whitespace reason as unset (no waiver applied, so CDS-SEC-074 still fires), so a schema that only enforced minLength: 1 would silently accept a value the code then discards."
|
|
260
|
+
}
|
|
261
|
+
}
|
|
234
262
|
}
|
|
235
263
|
}
|
|
236
264
|
}
|
|
@@ -49,6 +49,20 @@
|
|
|
49
49
|
"observability-leakage"
|
|
50
50
|
]
|
|
51
51
|
},
|
|
52
|
+
"complianceCategory": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "Informational compliance control category this rule's finding maps onto (e.g. for a user's own NIS2/Cyberbeveiligingswet risk assessment). This is a readiness aid, not a legal or regulatory certification of the rule or category -- see docs/security-compliance-categories.md.",
|
|
55
|
+
"enum": [
|
|
56
|
+
"access-control",
|
|
57
|
+
"secrets-management",
|
|
58
|
+
"network-exposure",
|
|
59
|
+
"encryption-in-transit",
|
|
60
|
+
"configuration-management",
|
|
61
|
+
"system-hardening",
|
|
62
|
+
"logging-monitoring",
|
|
63
|
+
"patching"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
52
66
|
"scopeItem": {
|
|
53
67
|
"type": "string",
|
|
54
68
|
"enum": [
|
|
@@ -183,6 +197,7 @@
|
|
|
183
197
|
"title",
|
|
184
198
|
"severity",
|
|
185
199
|
"category",
|
|
200
|
+
"complianceCategory",
|
|
186
201
|
"scope",
|
|
187
202
|
"match",
|
|
188
203
|
"message",
|
|
@@ -207,6 +222,9 @@
|
|
|
207
222
|
"category": {
|
|
208
223
|
"$ref": "#/$defs/category"
|
|
209
224
|
},
|
|
225
|
+
"complianceCategory": {
|
|
226
|
+
"$ref": "#/$defs/complianceCategory"
|
|
227
|
+
},
|
|
210
228
|
"scope": {
|
|
211
229
|
"type": "array",
|
|
212
230
|
"minItems": 1,
|
|
@@ -231,6 +249,11 @@
|
|
|
231
249
|
"enabled": {
|
|
232
250
|
"type": "boolean",
|
|
233
251
|
"default": true
|
|
252
|
+
},
|
|
253
|
+
"codeEnforced": {
|
|
254
|
+
"type": "boolean",
|
|
255
|
+
"default": true,
|
|
256
|
+
"description": "For a scope: [\"none\"] rule that is actually enforced entirely in code (not by the declarative match engine -- see the rule's $comment), this is the real on/off switch a custom rule set can flip to disable that code path. `enabled` stays false on such rules per the scope-none convention (it can never produce a declarative-engine finding), so `enabled` alone cannot represent whether the code-level check is active."
|
|
234
257
|
}
|
|
235
258
|
}
|
|
236
259
|
}
|
|
@@ -249,6 +272,7 @@
|
|
|
249
272
|
"title": "Hardcoded secret-like value",
|
|
250
273
|
"severity": "high",
|
|
251
274
|
"category": "secrets",
|
|
275
|
+
"complianceCategory": "secrets-management",
|
|
252
276
|
"scope": ["profile-raw", "module-values", "service-env"],
|
|
253
277
|
"match": {
|
|
254
278
|
"all": [
|