Skip to content

Commit f11fd59

Browse files
committed
fix(ci): close core runtime security gates
1 parent 72b9609 commit f11fd59

11 files changed

Lines changed: 223 additions & 5 deletions

‎Dockerfile.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ RUN pip install --no-cache-dir --upgrade pip \
4949
&& wheel="$(find /tmp/dist -name '*.whl' -print -quit)" \
5050
&& pip install --no-cache-dir --no-deps "${wheel}" \
5151
&& pip check \
52+
&& python -m pip uninstall --yes pip setuptools wheel \
5253
&& rm -rf /tmp/dist /tmp/requirements-docker.lock
5354

5455
# Non-root runtime (audit P1-4): this image ran as root by default. Helm

‎docs/PROJECT_CLOSURE.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,29 @@ rehearsal and third-party penetration test.
5757

5858
## Обязательные внешние closure gates
5959

60-
- local closing commits published to `main`;
60+
- closing commits `59fc19c` (Operator/Kafka reproducibility) and `72b9609`
61+
(Python compatibility/core lock remediation) published to `main`; the final
62+
core-only/Ruff/runtime-image hardening commit remains to be published;
6163
- all required GitHub checks green on the exact closing SHA;
6264
- two open Dependabot PRs receive a final one-by-one disposition;
6365
- the substantial post-v2 changes receive a final release/version decision and
6466
published supply-chain evidence if released.
6567

6668
Push, PR mutations and release actions require explicit owner authorization.
6769

70+
## Текущий closing handoff — 2026-07-30
71+
72+
- Python compatibility 3.11/3.12/3.13 is green on pushed SHA `72b9609`.
73+
- The remaining main-CI failures on that SHA (Ruff 0.16 SDK Markdown and
74+
core-only API import without `pyiceberg`) are fixed and locally verified.
75+
- The Security Scan findings were traced to packages vendored by runtime
76+
`pip`; the final image now removes installer tooling after `pip check`.
77+
- Independent Mac image validation reports zero HIGH/CRITICAL findings with
78+
Trivy 0.70.0; see
79+
[security-runtime-image-trivy-2026-07-30.md](security-runtime-image-trivy-2026-07-30.md).
80+
- Production status remains `candidate`; none of this closes the live Iceberg,
81+
restore/replay, fresh soak/rollback, external pen-test, or npm approval gates.
82+
6883
## Сохранённые локальные артефакты
6984

7085
Root `AGENTS.md` and `plan_sol_23_07_26` remain untracked and unchanged. They do

‎docs/STATUS.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
> Updated: **2026-07-30** (clean-checkout PyFlink OCI build + submission smoke
44
> **PASS**; Flink Kubernetes Operator + Helm golden-topology deploy **PASS**;
5+
> core-only API and hardened runtime-image remediation locally **PASS**;
56
> golden topology remains a production candidate, not production accepted) ·
67
> release line **`v2.0.0`**. Numbers below come only from measured, in-repo
78
> evidence — see the linked reports for methodology and reproduction commands.
@@ -21,6 +22,17 @@ and external security acceptance remain open. The earlier 4 h @ 100 eps result
2122
remains valid evidence for its measured pre-materializer path, not for the
2223
still-unaccepted full golden topology.
2324

25+
Closing CI hardening on 2026-07-30 also verified the Python 3.11/3.12/3.13
26+
compatibility lanes, made `pyiceberg` optional for the core-only API path,
27+
aligned SDK examples with Ruff 0.16 Markdown formatting, and removed
28+
`pip`/`setuptools`/`wheel` from the final API image after `pip check`.
29+
A clean core-only HTTP smoke returned 200 for health, entity, and NL query.
30+
An independent Mac rebuild scanned with Trivy 0.70.0 at zero HIGH/CRITICAL
31+
findings; see
32+
[security-runtime-image-trivy-2026-07-30.md](security-runtime-image-trivy-2026-07-30.md).
33+
The exact post-remediation GitHub SHA must still complete all required checks
34+
before the external closure gate is claimed.
35+
2436
**Project lifecycle:** closure candidate. Engineering scope is frozen; the
2537
production-candidate boundary, future acceptance program, and release gates are
2638
recorded in [PROJECT_CLOSURE.md](PROJECT_CLOSURE.md).
@@ -40,6 +52,7 @@ recorded in [PROJECT_CLOSURE.md](PROJECT_CLOSURE.md).
4052
| Multi-tenant ClickHouse write key | adversarial two-tenant suite green on live CH 25.3 (CI `test-integration` + audit stand) | [security-audit.md](security-audit.md), `tests/integration/test_clickhouse_tenant_isolation_live.py` |
4153
| Clean-checkout PyFlink OCI build + submission smoke | PASS clean-checkout OCI build + submission smoke on 2026-07-30 — image built, JobID `RUNNING`, not Operator/E2E | [perf/golden-flink-submission-2026-07-30.md](perf/golden-flink-submission-2026-07-30.md) |
4254
| Flink Kubernetes Operator + Helm golden deploy | PASS clean kind Operator/Helm deploy of verified image on exact HEAD `36ed1ec` — CR/job stable, checkpoints `2→23`, leader flaps `0`, not lake E2E | [perf/golden-operator-acceptance-2026-07-30.md](perf/golden-operator-acceptance-2026-07-30.md) |
55+
| Hardened API runtime image (local acceptance) | core-only API import/HTTP smoke PASS; Trivy 0.70.0 reports 0 HIGH/CRITICAL after runtime installer removal | [security-runtime-image-trivy-2026-07-30.md](security-runtime-image-trivy-2026-07-30.md) |
4356

4457
## 2026-07-23 audit closure
4558

‎docs/release-readiness.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ on the 3.11/3.12/3.13 `python-compat` matrix, so a failed compatibility lane
8383
cannot be bypassed merely because its job name is not a separate protected
8484
context.
8585

86+
The 2026-07-30 closing remediation additionally covers two clean-install
87+
boundaries: the core-only API imports and serves health/entity/query without
88+
the optional `pyiceberg` package, and the final API image removes
89+
`pip`/`setuptools`/`wheel` after its hash-locked install and `pip check`.
90+
Independent Mac validation with Trivy 0.70.0 reported zero HIGH/CRITICAL
91+
findings; details and evidence limits are in
92+
[security-runtime-image-trivy-2026-07-30.md](security-runtime-image-trivy-2026-07-30.md).
93+
All required GitHub checks must still be green on the exact final pushed SHA.
94+
8695
## Scope
8796

8897
Component, contract, Helm, and replay tests validate the checked-in streaming,

‎docs/security-audit.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,21 @@ The repository includes a dedicated security workflow in GitHub Actions:
154154
- Safety for dependency vulnerability scanning
155155
- Trivy for container image scanning and CycloneDX SBOM generation
156156

157+
On 2026-07-30, a Trivy scan of the API image identified vulnerable packages
158+
vendored by runtime `pip`, not dependencies from the application lock. The
159+
final stage now removes `pip`, `setuptools`, and `wheel` after the hash-locked
160+
install and `pip check`. An independent Mac rebuild and Trivy `0.70.0` scan
161+
reported zero HIGH/CRITICAL findings while the API import remained healthy.
162+
See
163+
[security-runtime-image-trivy-2026-07-30.md](security-runtime-image-trivy-2026-07-30.md).
164+
157165
The Bandit baseline currently records a historical `B310` finding in `src/serving/backends/clickhouse_backend.py`. SQL construction findings are not globally suppressed; reviewed identifier construction is handled through narrow suppressions and tests.
158166

159167
Helm defaults no longer embed production-shaped API-key verifier hashes. Operators can render a chart-managed Secret for local use or mount an existing Kubernetes Secret, which is friendlier to External Secrets Operator, Sealed Secrets, or equivalent workflows.
160168

161-
Evidence: `.github/workflows/security.yml`, `.bandit`, `.bandit-baseline.json`, `docs/helm-deployment.md`
169+
Evidence: `.github/workflows/security.yml`, `.bandit`, `.bandit-baseline.json`,
170+
`docs/helm-deployment.md`,
171+
`docs/security-runtime-image-trivy-2026-07-30.md`
162172

163173
## 9. Operational Security and Auditability
164174

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Runtime image Trivy remediation — 2026-07-30
2+
3+
## Scope
4+
5+
This record covers the API runtime image only. It does not claim a signed
6+
registry release, an external penetration test, or production acceptance.
7+
8+
## Trigger
9+
10+
GitHub Actions Security Scan run
11+
[`30570556301`](https://github.com/brownjuly2003-code/agentflow/actions/runs/30570556301)
12+
scanned commit `72b960930302ecbced46c89479a5b09082f0a406` with Trivy `0.70.0`.
13+
The image job reported:
14+
15+
- `GHSA-6v7p-g79w-8964`: vendored `msgpack 1.1.2`;
16+
- `CVE-2025-47273`: stale `setuptools 70.3.0`.
17+
18+
The uploaded CycloneDX SBOM associated both findings with the final installer
19+
layer and no application package path. The same layer listed other packages
20+
vendored by `pip`, while the actual installed setuptools distribution was the
21+
already-fixed `82.0.1`. Neither vulnerable version came from
22+
`requirements-docker.lock`.
23+
24+
## Remediation
25+
26+
`Dockerfile.api` still performs the hash-locked dependency install, installs
27+
the AgentFlow wheel with `--no-deps`, and runs `pip check`. It then removes
28+
runtime-only installer tooling:
29+
30+
```dockerfile
31+
python -m pip uninstall --yes pip setuptools wheel
32+
```
33+
34+
The builder stage is unchanged. A unit contract requires the uninstall to
35+
remain in the final stage and after `pip check`.
36+
37+
## Independent Mac validation
38+
39+
Validation used an isolated clone at
40+
`/tmp/agentflow-security-72b9609-codex-01`, based on exact commit
41+
`72b960930302ecbced46c89479a5b09082f0a406`, plus the candidate
42+
image-affecting files:
43+
44+
- `Dockerfile.api` blob `9cc33d81a88802991f632225a45f9422ab8e4358`;
45+
- `src/quality/monitors/metrics_collector.py` blob
46+
`475c656b0c7de9418e2447bd6b5cca36b9026146`.
47+
48+
The image was built from the pinned `python:3.11-slim` digest already declared
49+
in `Dockerfile.api`:
50+
51+
```text
52+
agentflow-api:security-codex-72b9609
53+
manifest list sha256:934357fe14e8f6ed3a712eb7ae8bec1a369aad06f54691eadd2b8aa7e2cd1591
54+
```
55+
56+
Build output confirmed that `pip 26.2`, `setuptools 82.0.1`, and `wheel 0.47.0`
57+
were uninstalled only after `pip check` returned
58+
`No broken requirements found`.
59+
60+
Runtime checks:
61+
62+
- `python -m pip --version` failed with `No module named pip` as intended;
63+
- `from src.serving.api.main import app` succeeded (`AgentFlow Query API`);
64+
- Trivy `0.70.0`, with `HIGH,CRITICAL`, `--ignore-unfixed`, and
65+
`--exit-code 1`, exited `0`;
66+
- Trivy summary: Debian `13.5` vulnerabilities `0`; every detected Python
67+
package vulnerabilities `0`.
68+
69+
## Evidence boundary
70+
71+
This is content-level local acceptance of the candidate runtime image. The
72+
next pushed SHA must still pass the repository's GitHub Security Scan and all
73+
other required checks. Published-image signing and external penetration-test
74+
evidence remain separate gates.

‎sdk/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ python -m pip install -e "./sdk"
2222

2323
```python
2424
from agentflow import AgentFlowClient
25+
2526
client = AgentFlowClient("http://localhost:8000", api_key="dev-key")
2627
order = client.get_order("ORD-20260404-1001")
2728
print(order.status, client.get_metric("revenue", "24h").value)
@@ -38,6 +39,7 @@ client.configure_resilience(retry_policy=RetryPolicy(max_attempts=5))
3839
```python
3940
from agentflow import AsyncAgentFlowClient
4041

42+
4143
async def main() -> None:
4244
async with AsyncAgentFlowClient("http://localhost:8000", api_key="dev-key") as client:
4345
order = await client.get_order("ORD-20260404-1001")

‎src/quality/monitors/metrics_collector.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import yaml
1919
from confluent_kafka import KafkaException
2020
from prometheus_client import Gauge
21-
from pyiceberg.exceptions import NoSuchPropertyException, RESTError, ValidationError
2221

2322
from src.serving.backends import BackendExecutionError
2423

@@ -379,6 +378,25 @@ def _check_iceberg(self) -> ComponentHealth:
379378
source=CheckSource.PLACEHOLDER,
380379
)
381380

381+
# Optional cloud extra: only import pyiceberg when a config file exists.
382+
# Local/core installs can construct HealthCollector(include_external=False)
383+
# without requiring the cloud dependency at module import time.
384+
try:
385+
from pyiceberg.exceptions import (
386+
NoSuchPropertyException,
387+
RESTError,
388+
ValidationError,
389+
)
390+
except ImportError as exc:
391+
return ComponentHealth(
392+
name="iceberg",
393+
status=HealthStatus.DEGRADED,
394+
message=f"Iceberg unavailable: {exc}",
395+
last_check=datetime.now(UTC),
396+
metrics={"row_counts": {}},
397+
source=CheckSource.PLACEHOLDER,
398+
)
399+
382400
try:
383401
from src.processing.iceberg_sink import IcebergSink
384402

‎tests/unit/test_contract_dependencies.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,15 @@ def test_docker_build_contexts_prepare_root_package_metadata():
207207
assert "--require-hashes -r /tmp/requirements-docker.lock" in text
208208
assert 'pip install --no-cache-dir --no-deps "${wheel}"' in text
209209
assert "pip check" in text
210+
# Runtime image must not ship installer tooling: Trivy SBOM tied
211+
# GHSA-6v7p-g79w-8964 (msgpack via pip vendor) and CVE-2025-47273
212+
# (setuptools) to the final-layer pip/setuptools/wheel tree, not
213+
# to requirements-docker.lock. Uninstall only after installs and
214+
# pip check so the hash-locked environment is still verified.
215+
final_stage = text.rsplit("FROM ", maxsplit=1)[-1]
216+
uninstall = "python -m pip uninstall --yes pip setuptools wheel"
217+
assert uninstall in final_stage
218+
assert final_stage.index("pip check") < final_stage.index(uninstall)
210219
# Q0.2 / S9: scale profile needs the Postgres control-plane driver
211220
# baked in (kind 2-pod previously required a docker-commit psycopg
212221
# hack). The wheel installs --no-deps, so the guarantee now lives

‎tests/unit/test_local_only_mode.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
from __future__ import annotations
44

5+
import os
6+
import subprocess
7+
import sys
8+
import tempfile
59
from pathlib import Path
610

711
import pytest
@@ -14,6 +18,8 @@
1418
from src.serving.api.main import app
1519
from src.serving.cache import QueryCache
1620

21+
PROJECT_ROOT = Path(__file__).resolve().parents[2]
22+
1723

1824
def test_local_health_collector_keeps_only_embedded_checks() -> None:
1925
collector = HealthCollector(include_external=False)
@@ -25,6 +31,66 @@ def test_local_health_collector_keeps_only_embedded_checks() -> None:
2531
]
2632

2733

34+
def test_metrics_collector_imports_without_pyiceberg() -> None:
35+
"""Core/local mode must import HealthCollector when pyiceberg is absent."""
36+
script = """
37+
import os
38+
import sys
39+
40+
# Force pyiceberg absent even if the host env has the cloud extra installed.
41+
sys.modules["pyiceberg"] = None
42+
sys.modules["pyiceberg.exceptions"] = None
43+
44+
from src.quality.monitors.metrics_collector import (
45+
CheckSource,
46+
HealthCollector,
47+
HealthStatus,
48+
)
49+
50+
collector = HealthCollector(include_external=False)
51+
assert [check.__name__ for check in collector._checks] == [
52+
"_check_serving",
53+
"_check_freshness",
54+
"_check_quality_score",
55+
]
56+
57+
# Existing config + blocked pyiceberg must yield degraded placeholder, not
58+
# the missing-config path (config check runs before the lazy import).
59+
health = collector._check_iceberg()
60+
assert health.name == "iceberg"
61+
assert health.status == HealthStatus.DEGRADED
62+
assert health.source == CheckSource.PLACEHOLDER
63+
assert health.message.startswith("Iceberg unavailable")
64+
assert "Iceberg config not found" not in health.message
65+
print("ok")
66+
"""
67+
with tempfile.NamedTemporaryFile(
68+
mode="w",
69+
suffix=".yaml",
70+
delete=False,
71+
encoding="utf-8",
72+
) as handle:
73+
handle.write("catalog:\n name: test\n")
74+
config_path = handle.name
75+
76+
env = os.environ.copy()
77+
env["AGENTFLOW_ICEBERG_CONFIG"] = config_path
78+
try:
79+
result = subprocess.run(
80+
[sys.executable, "-c", script],
81+
cwd=PROJECT_ROOT,
82+
capture_output=True,
83+
text=True,
84+
check=False,
85+
env=env,
86+
)
87+
finally:
88+
Path(config_path).unlink(missing_ok=True)
89+
90+
assert result.returncode == 0, result.stderr
91+
assert "ok" in result.stdout
92+
93+
2894
def test_disabled_query_cache_does_not_construct_redis(
2995
monkeypatch: pytest.MonkeyPatch,
3096
) -> None:

0 commit comments

Comments
 (0)