Compare commits

..
Author SHA1 Message Date
Sharang ParnerkarandClaude Opus 4.8 9c8f864a1d docs(control-mapping): MCP emission loop + flags now default-on
CI / Check (push) Skipped
CI / Check (pull_request) Successful in 5m47s
CI / Detect Changes (pull_request) Skipped
CI / Deploy Agent (pull_request) Skipped
CI / Deploy Dashboard (pull_request) Skipped
CI / Deploy Docs (pull_request) Skipped
CI / Deploy MCP (pull_request) Skipped
- Add 'Emitting over MCP' section: the oscal_assessment tool, breakpilot's
  /v1/cra/oscal-from-scanner pull, and the tenant-context operational note
  (task_local lost across rmcp's session spawn -> bind tenant to the session).
- Configuration: semantic + grounded passes are now default-on (validated live),
  still no-op without BREAKPILOT_BASE_URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 13:14:59 +02:00
5 changed files with 19 additions and 72 deletions
+11 -11
View File
@@ -72,7 +72,7 @@ jobs:
echo '[source.crates-io]' echo '[source.crates-io]'
echo 'replace-with = "kellnr"' echo 'replace-with = "kellnr"'
echo '[registries.kellnr]' echo '[registries.kellnr]'
echo 'index = "sparse+https://crates.breakpilot.com/api/v1/cratesio/"' echo 'index = "sparse+https://crates.meghsakha.com/api/v1/cratesio/"'
} >> "$CARGO_HOME/config.toml" } >> "$CARGO_HOME/config.toml"
env: env:
RUSTC_WRAPPER: "" RUSTC_WRAPPER: ""
@@ -94,8 +94,8 @@ jobs:
- name: Configure git auth for private tramiton dependency - name: Configure git auth for private tramiton dependency
run: | run: |
git config --global \ git config --global \
url."https://sharang:${{ secrets.TRAMITON_FETCH_TOKEN }}@git.breakpilot.com/".insteadOf \ url."https://sharang:${{ secrets.TRAMITON_FETCH_TOKEN }}@gitea.meghsakha.com/".insteadOf \
"ssh://git@git.breakpilot.com:22222/" "ssh://git@gitea.meghsakha.com:22222/"
env: env:
RUSTC_WRAPPER: "" RUSTC_WRAPPER: ""
@@ -213,8 +213,8 @@ jobs:
apk add --no-cache git curl openssl apk add --no-cache git curl openssl
git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD
IMAGE=repo.breakpilot.com/certifai/compliance-agent IMAGE=repo.meghsakha.com/certifai/compliance-agent
echo "$REGISTRY_PASSWORD" | docker login repo.breakpilot.com -u "$REGISTRY_USERNAME" --password-stdin echo "$REGISTRY_PASSWORD" | docker login repo.meghsakha.com -u "$REGISTRY_USERNAME" --password-stdin
DOCKER_BUILDKIT=1 docker build --secret id=tramiton_token,env=TRAMITON_FETCH_TOKEN \ DOCKER_BUILDKIT=1 docker build --secret id=tramiton_token,env=TRAMITON_FETCH_TOKEN \
-f Dockerfile.agent -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" . -f Dockerfile.agent -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" .
docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}" docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}"
@@ -240,8 +240,8 @@ jobs:
apk add --no-cache git curl openssl apk add --no-cache git curl openssl
git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD
IMAGE=repo.breakpilot.com/certifai/compliance-dashboard IMAGE=repo.meghsakha.com/certifai/compliance-dashboard
echo "$REGISTRY_PASSWORD" | docker login repo.breakpilot.com -u "$REGISTRY_USERNAME" --password-stdin echo "$REGISTRY_PASSWORD" | docker login repo.meghsakha.com -u "$REGISTRY_USERNAME" --password-stdin
DOCKER_BUILDKIT=1 docker build --secret id=tramiton_token,env=TRAMITON_FETCH_TOKEN \ DOCKER_BUILDKIT=1 docker build --secret id=tramiton_token,env=TRAMITON_FETCH_TOKEN \
-f Dockerfile.dashboard -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" . -f Dockerfile.dashboard -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" .
docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}" docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}"
@@ -265,8 +265,8 @@ jobs:
apk add --no-cache git curl openssl apk add --no-cache git curl openssl
git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD
IMAGE=repo.breakpilot.com/certifai/compliance-docs IMAGE=repo.meghsakha.com/certifai/compliance-docs
echo "$REGISTRY_PASSWORD" | docker login repo.breakpilot.com -u "$REGISTRY_USERNAME" --password-stdin echo "$REGISTRY_PASSWORD" | docker login repo.meghsakha.com -u "$REGISTRY_USERNAME" --password-stdin
docker build -f Dockerfile.docs -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" . docker build -f Dockerfile.docs -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" .
docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}" docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}"
{ command -v cosign >/dev/null 2>&1 || curl -sSfLo /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/download/v2.4.3/cosign-linux-amd64 || wget -qO /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/download/v2.4.3/cosign-linux-amd64; } || echo "::warning::cosign fetch failed" { command -v cosign >/dev/null 2>&1 || curl -sSfLo /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/download/v2.4.3/cosign-linux-amd64 || wget -qO /usr/local/bin/cosign https://github.com/sigstore/cosign/releases/download/v2.4.3/cosign-linux-amd64; } || echo "::warning::cosign fetch failed"
@@ -291,8 +291,8 @@ jobs:
apk add --no-cache git curl openssl apk add --no-cache git curl openssl
git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" git init && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD git fetch --depth=1 origin "${GITHUB_SHA}" && git checkout FETCH_HEAD
IMAGE=repo.breakpilot.com/certifai/compliance-mcp IMAGE=repo.meghsakha.com/certifai/compliance-mcp
echo "$REGISTRY_PASSWORD" | docker login repo.breakpilot.com -u "$REGISTRY_USERNAME" --password-stdin echo "$REGISTRY_PASSWORD" | docker login repo.meghsakha.com -u "$REGISTRY_USERNAME" --password-stdin
DOCKER_BUILDKIT=1 docker build --secret id=tramiton_token,env=TRAMITON_FETCH_TOKEN \ DOCKER_BUILDKIT=1 docker build --secret id=tramiton_token,env=TRAMITON_FETCH_TOKEN \
-f Dockerfile.mcp -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" . -f Dockerfile.mcp -t "$IMAGE:latest" -t "$IMAGE:${GITHUB_SHA}" .
docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}" docker push "$IMAGE:latest" && docker push "$IMAGE:${GITHUB_SHA}"
+2 -2
View File
@@ -8,8 +8,8 @@ COPY . .
RUN --mount=type=secret,id=tramiton_token \ RUN --mount=type=secret,id=tramiton_token \
if [ -s /run/secrets/tramiton_token ]; then \ if [ -s /run/secrets/tramiton_token ]; then \
git config --global \ git config --global \
url."https://sharang:$(cat /run/secrets/tramiton_token)@git.breakpilot.com/".insteadOf \ url."https://sharang:$(cat /run/secrets/tramiton_token)@gitea.meghsakha.com/".insteadOf \
"ssh://git@git.breakpilot.com:22222/"; \ "ssh://git@gitea.meghsakha.com:22222/"; \
fi && \ fi && \
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release -p compliance-agent CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release -p compliance-agent
+2 -2
View File
@@ -13,8 +13,8 @@ ENV DOCS_URL=${DOCS_URL}
RUN --mount=type=secret,id=tramiton_token \ RUN --mount=type=secret,id=tramiton_token \
if [ -s /run/secrets/tramiton_token ]; then \ if [ -s /run/secrets/tramiton_token ]; then \
git config --global \ git config --global \
url."https://sharang:$(cat /run/secrets/tramiton_token)@git.breakpilot.com/".insteadOf \ url."https://sharang:$(cat /run/secrets/tramiton_token)@gitea.meghsakha.com/".insteadOf \
"ssh://git@git.breakpilot.com:22222/"; \ "ssh://git@gitea.meghsakha.com:22222/"; \
fi && \ fi && \
CARGO_NET_GIT_FETCH_WITH_CLI=true dx build --release --package compliance-dashboard CARGO_NET_GIT_FETCH_WITH_CLI=true dx build --release --package compliance-dashboard
+2 -2
View File
@@ -8,8 +8,8 @@ COPY . .
RUN --mount=type=secret,id=tramiton_token \ RUN --mount=type=secret,id=tramiton_token \
if [ -s /run/secrets/tramiton_token ]; then \ if [ -s /run/secrets/tramiton_token ]; then \
git config --global \ git config --global \
url."https://sharang:$(cat /run/secrets/tramiton_token)@git.breakpilot.com/".insteadOf \ url."https://sharang:$(cat /run/secrets/tramiton_token)@gitea.meghsakha.com/".insteadOf \
"ssh://git@git.breakpilot.com:22222/"; \ "ssh://git@gitea.meghsakha.com:22222/"; \
fi && \ fi && \
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release -p compliance-mcp CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release -p compliance-mcp
+2 -55
View File
@@ -277,10 +277,8 @@ impl PipelineOrchestrator {
} }
} }
// Dedup against existing findings: insert first-seen ones, and refresh the // Dedup against existing findings and insert new ones
// control mappings on ones we've seen before.
let mut new_count = 0u32; let mut new_count = 0u32;
let mut refreshed_count = 0u32;
let mut new_findings: Vec<Finding> = Vec::new(); let mut new_findings: Vec<Finding> = Vec::new();
for mut finding in all_findings { for mut finding in all_findings {
finding.scan_run_id = Some(scan_run_id.to_string()); finding.scan_run_id = Some(scan_run_id.to_string());
@@ -295,25 +293,8 @@ impl PipelineOrchestrator {
finding.id = result.inserted_id.as_object_id(); finding.id = result.inserted_id.as_object_id();
new_findings.push(finding); new_findings.push(finding);
new_count += 1; new_count += 1;
} else if !finding.control_refs.is_empty() {
// Re-scan refresh: a mapping pass (newly enabled or tuned) computed
// control_refs for a finding first seen before mapping ran. Persist
// them onto the existing row — the insert path alone never would.
self.db
.findings()
.update_one(
doc! { "fingerprint": &finding.fingerprint },
doc! { "$set": { "control_refs": finding.control_refs.clone() } },
)
.await?;
refreshed_count += 1;
} }
} }
if refreshed_count > 0 {
tracing::info!(
"[{repo_id}] Refreshed control_refs on {refreshed_count} existing findings"
);
}
// Remove stale SBOM entries for this repo before reinserting // Remove stale SBOM entries for this repo before reinserting
if !sbom_entries.is_empty() { if !sbom_entries.is_empty() {
@@ -586,21 +567,7 @@ impl PipelineOrchestrator {
let Some(path) = ingest_set.get(&a.id).and_then(|ia| ia.working_path.clone()) else { let Some(path) = ingest_set.get(&a.id).and_then(|ia| ia.working_path.clone()) else {
continue; continue;
}; };
let mut source_findings = crate::pipeline::plc::analyze_tree(&path, target_id); all_findings.extend(crate::pipeline::plc::analyze_tree(&path, target_id));
// Control mapping for the PLC path (run_plc_scan is separate from
// run_pipeline, which does its own mapping). PLC findings carry
// file_path/line/cwe, so the semantic pass reads each region under this
// source's `path` and stamps master-control refs. The LUT + grounded
// surface passes are code-pattern / CRA-specific and don't apply to
// IEC 61131-3 control logic, so only the semantic pass runs here.
crate::controls::semantic_stamp_findings(
&self.config,
self.llm.clone(),
&path,
&mut source_findings,
)
.await;
all_findings.extend(source_findings);
// Control-application SBOM: CODESYS libraries + runtime from a // Control-application SBOM: CODESYS libraries + runtime from a
// `.projectarchive` (uploaded, or committed in the working tree). // `.projectarchive` (uploaded, or committed in the working tree).
let archive = a let archive = a
@@ -625,7 +592,6 @@ impl PipelineOrchestrator {
); );
let mut new_count = 0u32; let mut new_count = 0u32;
let mut refreshed_count = 0u32;
for mut finding in all_findings { for mut finding in all_findings {
finding.scan_run_id = Some(scan_run_id.to_string()); finding.scan_run_id = Some(scan_run_id.to_string());
if self if self
@@ -637,27 +603,8 @@ impl PipelineOrchestrator {
{ {
self.db.findings().insert_one(&finding).await?; self.db.findings().insert_one(&finding).await?;
new_count += 1; new_count += 1;
} else if !finding.control_refs.is_empty() {
// Re-scan refresh: mirror run_pipeline — persist newly-computed
// control_refs onto a PLC finding first seen before the semantic
// pass ran. The insert path alone never would, so without this a
// PLC re-scan can only pick up mappings via a delete + re-add.
self.db
.findings()
.update_one(
doc! { "fingerprint": &finding.fingerprint },
doc! { "$set": { "control_refs": finding.control_refs.clone() } },
)
.await?;
refreshed_count += 1;
} }
} }
if refreshed_count > 0 {
tracing::info!(
target_id,
"Refreshed control_refs on {refreshed_count} existing PLC findings"
);
}
if !all_sbom.is_empty() { if !all_sbom.is_empty() {
if let Err(e) = self if let Err(e) = self