feat(audit): P106 MC-Audit-Type + P83 BUILD_SHA in Dockerfiles + P80 v2 full
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 16s
CI / detect-changes (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 16s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m42s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 41s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 16s
CI / detect-changes (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 16s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m42s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 41s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
P106 — mc_audit_type.py: zentrales Quality-Thema. Klassifiziert pro MC: verifiable / process_internal / doc_internal / ambiguous. Pattern-Match auf check_question + title + fail_criteria (Schulung, AVV abgeschlossen, TOM umgesetzt, DSFA durchgefuehrt, Ausnahmen dokumentieren, kostenfrei zur Verfuegung, opt-out intern ermoeglichen, …). Interne MCs werden in der MC-Auswertung NICHT mehr als FAIL gewertet, sondern als CHECK markiert (audit_status='check'). Sie zaehlen im build_scorecard als skipped (nicht failed) damit der Score realistisch ist. build_internal_checks_block_html() rendert sie als separaten blauen Block 'Pruefungen die wir von aussen NICHT durchfuehren koennen' nach dem MC-Scorecard. Erwartete Wirkung: bei VW 95 FAILs → wahrscheinlich 30-40 echte verifiable_fails + 50-60 internal_checks. GF-Mail wird drastisch realistischer (statt 'Sie haben 95 Verstoesse' → 'Sie haben 35 extern sichtbare Themen + 60 interne Checks, bitte mit DSB klaeren'). P83 — BUILD_SHA in backend/admin/consent-tester Dockerfiles als ARG + ENV. check-rebuild-needed.sh kann jetzt deployed vs local SHA vergleichen + REBUILD REQUIRED melden. P80 v2 — check_replay.py macht jetzt vollstaendigen Replay aller post-fetch Quality-Generatoren: vendor_normalizer (Dedup), audit_quality_checks, cookie_compliance_audit, tcf_vendor_authority, cookie_value_entropy, cookie_network_tracer. Snapshots aus alter Zeit zeigen jetzt im Replay den aktuellen Audit-Stand. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,5 +55,9 @@ EXPOSE 3000
|
|||||||
# Set hostname
|
# Set hostname
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
# P83 — Build-SHA fuer check-rebuild-needed.sh
|
||||||
|
ARG BUILD_SHA="unknown"
|
||||||
|
ENV BUILD_SHA=${BUILD_SHA}
|
||||||
|
|
||||||
# Start the application
|
# Start the application
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -60,5 +60,9 @@ EXPOSE 8002
|
|||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
CMD curl -f http://127.0.0.1:8002/health || exit 1
|
CMD curl -f http://127.0.0.1:8002/health || exit 1
|
||||||
|
|
||||||
|
# P83 — Build-SHA fuer check-rebuild-needed.sh
|
||||||
|
ARG BUILD_SHA="unknown"
|
||||||
|
ENV BUILD_SHA=${BUILD_SHA}
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"]
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"]
|
||||||
|
|||||||
@@ -1184,6 +1184,22 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
|||||||
if (not c.passed and not c.skipped
|
if (not c.passed and not c.skipped
|
||||||
and (c.severity or "").upper() in ("CRITICAL", "HIGH")):
|
and (c.severity or "").upper() in ("CRITICAL", "HIGH")):
|
||||||
fails_by_doc.setdefault(r.doc_type, []).append(rec)
|
fails_by_doc.setdefault(r.doc_type, []).append(rec)
|
||||||
|
# P106 — Audit-Type-Klassifizierung pro MC. Interne Prozess-/
|
||||||
|
# Doku-Checks werden NICHT als FAIL gewertet sondern als CHECK
|
||||||
|
# (manuelle Pruefung beim DSB notwendig).
|
||||||
|
try:
|
||||||
|
from compliance.services.mc_audit_type import (
|
||||||
|
annotate_mc_results, split_by_audit_type,
|
||||||
|
)
|
||||||
|
annotate_mc_results(all_mc_checks)
|
||||||
|
mc_split = split_by_audit_type(all_mc_checks)
|
||||||
|
# Fails-by-doc neu aufbauen: nur noch echte verifiable Fails
|
||||||
|
fails_by_doc = {}
|
||||||
|
for r in mc_split.get("verifiable_fails") or []:
|
||||||
|
fails_by_doc.setdefault("dse", []).append(r)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("P106 mc_audit_type skipped: %s", e)
|
||||||
|
mc_split = {"internal_checks": [], "verifiable_fails": all_mc_checks}
|
||||||
scorecard = build_scorecard(all_mc_checks) if all_mc_checks else {}
|
scorecard = build_scorecard(all_mc_checks) if all_mc_checks else {}
|
||||||
# Trend: load previous scorecard for the same tenant + domain so the
|
# Trend: load previous scorecard for the same tenant + domain so the
|
||||||
# email can show delta indicators (A6).
|
# email can show delta indicators (A6).
|
||||||
@@ -1503,6 +1519,22 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("industry context skipped: %s", e)
|
logger.warning("industry context skipped: %s", e)
|
||||||
|
|
||||||
|
# P106 — Internal-Checks-Block (interne Prozesse / Doku-Pflichten)
|
||||||
|
internal_checks_html = ""
|
||||||
|
try:
|
||||||
|
from compliance.services.mc_audit_type import (
|
||||||
|
build_internal_checks_block_html,
|
||||||
|
)
|
||||||
|
ic = (mc_split or {}).get("internal_checks") or []
|
||||||
|
if ic:
|
||||||
|
internal_checks_html = build_internal_checks_block_html(ic)
|
||||||
|
logger.info(
|
||||||
|
"P106: %d interne Checks (statt FAIL) im Block",
|
||||||
|
len(ic),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("P106 internal_checks_html skipped: %s", e)
|
||||||
|
|
||||||
# P85 — Banner-Screenshot fuer visuellen Beweis (zwischen
|
# P85 — Banner-Screenshot fuer visuellen Beweis (zwischen
|
||||||
# GF-1-Pager und Detail-Bloecken)
|
# GF-1-Pager und Detail-Bloecken)
|
||||||
banner_shot_html = ""
|
banner_shot_html = ""
|
||||||
@@ -1612,7 +1644,7 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
|||||||
+ bench_html + diff_html
|
+ bench_html + diff_html
|
||||||
+ critical_html + scope_disclaimer_html + exec_summary_html
|
+ critical_html + scope_disclaimer_html + exec_summary_html
|
||||||
+ cookie_arch_html + summary_html + scanned_html + profile_html
|
+ cookie_arch_html + summary_html + scanned_html + profile_html
|
||||||
+ scorecard_html + redundancy_html
|
+ scorecard_html + internal_checks_html + redundancy_html
|
||||||
+ industry_ctx_html
|
+ industry_ctx_html
|
||||||
+ banner_shot_html
|
+ banner_shot_html
|
||||||
+ providers_html + banner_deep_html
|
+ providers_html + banner_deep_html
|
||||||
|
|||||||
@@ -86,27 +86,81 @@ def replay_from_snapshot(
|
|||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
|
|
||||||
# P80 v2 — Quality-Checks aus dem aktuellen Code auf Snapshot-Daten
|
# P80 v2 — Quality-Checks aus dem aktuellen Code auf Snapshot-Daten
|
||||||
# anwenden. Wir replayen NICHT die MC-Pipeline (zu schwer ohne
|
# anwenden. Vollstaendiger Replay aller post-fetch Findings-Generatoren.
|
||||||
# rag_document_checker re-run), aber alle nachgelagerten Findings-
|
cookie_t = doc_texts.get("cookie") or doc_texts.get("dse") or ""
|
||||||
# Generatoren (audit_quality, cookie_compliance_audit, vendor_normalizer,
|
|
||||||
# entropy, network-trace) bekommen Snapshot-Daten und liefern den
|
# Vendor-Normalize (Dedup + Garbage-Filter)
|
||||||
# aktuellen Stand.
|
try:
|
||||||
|
from compliance.services.vendor_normalizer import normalize_vendors
|
||||||
|
cmp_vendors = normalize_vendors(list(cmp_vendors))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Replay v2: normalizer failed: %s", e)
|
||||||
|
|
||||||
|
# Audit-Quality
|
||||||
try:
|
try:
|
||||||
from compliance.services.audit_quality_checks import (
|
from compliance.services.audit_quality_checks import (
|
||||||
run_all as run_aq,
|
run_all as run_aq, build_audit_quality_block_html,
|
||||||
)
|
)
|
||||||
cookie_t = doc_texts.get("cookie") or doc_texts.get("dse") or ""
|
|
||||||
aq = run_aq(banner_result, cookie_t, cmp_vendors, doc_entries)
|
aq = run_aq(banner_result, cookie_t, cmp_vendors, doc_entries)
|
||||||
if aq:
|
if aq:
|
||||||
from compliance.services.audit_quality_checks import (
|
|
||||||
build_audit_quality_block_html,
|
|
||||||
)
|
|
||||||
aq_html = build_audit_quality_block_html(aq)
|
aq_html = build_audit_quality_block_html(aq)
|
||||||
parts.append(aq_html)
|
parts.append(aq_html)
|
||||||
section_sizes["audit_quality_v2"] = len(aq_html)
|
section_sizes["audit_quality_v2"] = len(aq_html)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Replay v2: audit_quality failed: %s", e)
|
logger.warning("Replay v2: audit_quality failed: %s", e)
|
||||||
|
|
||||||
|
# Cookie-Compliance-Audit
|
||||||
|
try:
|
||||||
|
from compliance.services.cookie_compliance_audit import (
|
||||||
|
audit_cookie_compliance, build_cookie_audit_block_html,
|
||||||
|
)
|
||||||
|
ca = audit_cookie_compliance(db, cookie_t, banner_result)
|
||||||
|
if ca and (ca.get("declared_count") or ca.get("browser_count")):
|
||||||
|
ca_html = build_cookie_audit_block_html(ca)
|
||||||
|
parts.append(ca_html)
|
||||||
|
section_sizes["cookie_audit_v2"] = len(ca_html)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Replay v2: cookie_audit failed: %s", e)
|
||||||
|
|
||||||
|
# TCF Authority
|
||||||
|
try:
|
||||||
|
from compliance.services.tcf_vendor_authority import (
|
||||||
|
cross_reference_with_tcf, build_tcf_authority_block_html,
|
||||||
|
)
|
||||||
|
tcf = cross_reference_with_tcf(db, cmp_vendors)
|
||||||
|
if tcf:
|
||||||
|
tcf_html = build_tcf_authority_block_html(tcf)
|
||||||
|
parts.append(tcf_html)
|
||||||
|
section_sizes["tcf_v2"] = len(tcf_html)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Replay v2: tcf failed: %s", e)
|
||||||
|
|
||||||
|
# Entropy + Network-Trace
|
||||||
|
try:
|
||||||
|
from compliance.services.cookie_value_entropy import (
|
||||||
|
check_cookies_for_entropy_mismatch, build_entropy_block_html,
|
||||||
|
)
|
||||||
|
from compliance.services.cookie_network_tracer import (
|
||||||
|
trace_cookie_network, build_network_trace_block_html,
|
||||||
|
)
|
||||||
|
cd = (banner_result or {}).get("cookies_detailed") or []
|
||||||
|
e1 = check_cookies_for_entropy_mismatch(cd)
|
||||||
|
if e1:
|
||||||
|
ent_html = build_entropy_block_html(e1)
|
||||||
|
parts.append(ent_html)
|
||||||
|
section_sizes["entropy_v2"] = len(ent_html)
|
||||||
|
site_url = ""
|
||||||
|
for entry in (doc_entries or []):
|
||||||
|
if entry.get("url"):
|
||||||
|
site_url = entry["url"]; break
|
||||||
|
net = trace_cookie_network(cd, site_url)
|
||||||
|
if net:
|
||||||
|
net_html = build_network_trace_block_html(net)
|
||||||
|
parts.append(net_html)
|
||||||
|
section_sizes["network_trace_v2"] = len(net_html)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Replay v2: entropy/network failed: %s", e)
|
||||||
|
|
||||||
# P82: GF-1-Pager zuerst (5-Bullet-Summary)
|
# P82: GF-1-Pager zuerst (5-Bullet-Summary)
|
||||||
try:
|
try:
|
||||||
from compliance.services.gf_one_pager import build_gf_one_pager_html
|
from compliance.services.gf_one_pager import build_gf_one_pager_html
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
"""
|
||||||
|
P106 — MC-Audit-Type-Klassifizierung.
|
||||||
|
|
||||||
|
Zentrales Problem: viele Master-Controls pruefen Sachverhalte, die wir
|
||||||
|
von Aussen GAR NICHT pruefen koennen — z.B. ob das Unternehmen einen
|
||||||
|
internen Loeschkonzept-Prozess hat oder Schulungen durchgefuehrt wurden.
|
||||||
|
|
||||||
|
Bisher: alle MCs deren Pattern im Text nicht matched → FAIL.
|
||||||
|
Folge: GF-Mail mit 95 FAILs, davon ~60-70 in Wirklichkeit nur 'unknown'.
|
||||||
|
|
||||||
|
Loesung: pro MC klassifizieren:
|
||||||
|
* verifiable → Pattern muss im sichtbaren Dokument stehen (Audit moeglich)
|
||||||
|
* process_internal → interner Prozess des Kunden (Schulung, AVV-Vertrag, …)
|
||||||
|
* doc_internal → interne Dokumentation (VVT-Eintrag, DSFA-File, …)
|
||||||
|
* ambiguous → koennte beides sein
|
||||||
|
|
||||||
|
In der MC-Auswertung:
|
||||||
|
* verifiable + Pattern fehlt → echtes FAIL ❌
|
||||||
|
* process_internal → CHECK (Hinweis 'Bitte intern pruefen') ⓘ
|
||||||
|
* doc_internal → CHECK (Hinweis 'Im VVT/DSFA dokumentiert?') ⓘ
|
||||||
|
* ambiguous → CHECK mit Warnung
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Patterns die auf interne Prozesse hindeuten (NICHT von aussen pruefbar)
|
||||||
|
_PROCESS_INTERNAL_PATTERNS = [
|
||||||
|
# Schulung / Mitarbeiter
|
||||||
|
r"\bmitarbeiter\b.*schul",
|
||||||
|
r"\bschulung(en)?\b",
|
||||||
|
r"\bawareness\b",
|
||||||
|
r"\bsensibilisier",
|
||||||
|
# Vertraege intern
|
||||||
|
r"\bauftragsverarbeitungsvertrag\b",
|
||||||
|
r"\bAVV\b\s+abgeschlossen",
|
||||||
|
r"\bvertrag.*abgeschlossen",
|
||||||
|
r"\bdpa\s+(geschlossen|abgeschlossen|vorhanden)",
|
||||||
|
r"\bSCC\s+(geschlossen|abgeschlossen|implementiert)",
|
||||||
|
# Technisch-organisatorische Massnahmen (intern)
|
||||||
|
r"\btechnisch[-\s]*organisatorische\s+ma(ß|ss)nahmen?\b",
|
||||||
|
r"\bTOM\s+(umgesetzt|dokumentiert|implementiert)",
|
||||||
|
r"\bverschluesselung\s+(implementiert|aktiv)",
|
||||||
|
r"\bpseudonymisierung\s+(implementiert|aktiv)",
|
||||||
|
r"\bbackup[s]?\s+(eingerichtet|vorhanden)",
|
||||||
|
r"\bzugriffskontrolle",
|
||||||
|
r"\b(rollen|berechtigungs)konzept",
|
||||||
|
# Risikobewertung / DSFA (intern)
|
||||||
|
r"\bdsfa\s+(durchgefuehrt|erstellt|dokumentiert)",
|
||||||
|
r"\brisikobewertung\s+(durchgefuehrt|dokumentiert)",
|
||||||
|
r"\brisikoanalyse",
|
||||||
|
# Loeschkonzept / Aufbewahrung
|
||||||
|
r"\bloeschkonzept\s+(umgesetzt|implementiert)",
|
||||||
|
r"\baufbewahrungsfrist(en)?\s+(eingehalten|definiert)",
|
||||||
|
r"\bloeschroutinen?\s+(aktiv|implementiert)",
|
||||||
|
# Meldewege / Vorfallmanagement
|
||||||
|
r"\bmeldepflicht\s+(eingehalten|umgesetzt)",
|
||||||
|
r"\bvorfallmanagement",
|
||||||
|
r"\bincident[\s-]?response",
|
||||||
|
r"\b72[\s-]?stunden[\s-]?meldung",
|
||||||
|
# Generische Prozess-Indikatoren
|
||||||
|
r"\bdokumentiert\s+werden",
|
||||||
|
r"\bbitte\s+(intern\s+)?dokumentieren",
|
||||||
|
r"\bin\s+der\s+verfahrens",
|
||||||
|
r"\bnach\s+innen\s+geh",
|
||||||
|
r"\bausnahmen\s+(dokumentieren|protokollieren)",
|
||||||
|
r"\bkostenfrei\s+(zur\s+verfuegung|gewaehren|ermoegli)",
|
||||||
|
r"\bunentgeltlich\s+(zur\s+verfuegung)",
|
||||||
|
# Vertragsleistung / Service-Level (intern)
|
||||||
|
r"\bservice[\s-]?level",
|
||||||
|
r"\breaktionszeit",
|
||||||
|
# Auditierung / Aufsicht
|
||||||
|
r"\binterne(s)?\s+audit",
|
||||||
|
r"\baufsichtsbehoerde\s+gemeldet",
|
||||||
|
r"\bbeauftragter\s+(intern|benannt)",
|
||||||
|
# eCall + Branchen-spezifische interne Pflichten
|
||||||
|
r"\babschaltung\s+der\s+\w+\s+kostenfrei",
|
||||||
|
r"\bopt[\s-]?out\s+(intern|im\s+kundenportal)\s+ermoeglichen",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Patterns die auf interne Dokumentation hindeuten (VVT, DSFA-Datei, …)
|
||||||
|
_DOC_INTERNAL_PATTERNS = [
|
||||||
|
r"\bverzeichnis\s+der\s+verarbeitungstaetigkeiten\b",
|
||||||
|
r"\bvvt(\s+|\b)",
|
||||||
|
r"\bdsfa[\s-]?dokument",
|
||||||
|
r"\bauftragsverarbeitungsverzeichnis",
|
||||||
|
r"\bsub[\s-]?prozessor[\s-]?liste",
|
||||||
|
r"\bverarbeitungs[\s-]?register",
|
||||||
|
r"\binternes\s+register",
|
||||||
|
r"\baufbewahrungs[\s-]?konzept\b",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Patterns die auf externe Sichtbarkeit hindeuten → DEFINITIV verifiable
|
||||||
|
_VERIFIABLE_PATTERNS = [
|
||||||
|
r"\bin\s+der\s+(datenschutzerklaerung|dse|cookie[\s-]?richtlinie|impressum|agb)\b",
|
||||||
|
r"\bauf\s+der\s+website\s+(genannt|sichtbar|angegeben)",
|
||||||
|
r"\bim\s+banner\s+(genannt|sichtbar)",
|
||||||
|
r"\bim\s+cookie[\s-]?banner",
|
||||||
|
r"\bauf\s+der\s+startseite",
|
||||||
|
r"\bim\s+footer",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_any(text: str, patterns: list[str]) -> bool:
|
||||||
|
tl = text.lower()
|
||||||
|
for pat in patterns:
|
||||||
|
try:
|
||||||
|
if re.search(pat, tl):
|
||||||
|
return True
|
||||||
|
except re.error:
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def classify_mc_audit_type(
|
||||||
|
title: str | None,
|
||||||
|
check_question: str | None = None,
|
||||||
|
fail_criteria: dict | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Returns 'verifiable', 'process_internal', 'doc_internal',
|
||||||
|
or 'ambiguous'."""
|
||||||
|
blob = " ".join([title or "", check_question or "",
|
||||||
|
str(fail_criteria or "")])
|
||||||
|
if not blob.strip():
|
||||||
|
return "ambiguous"
|
||||||
|
|
||||||
|
is_verifiable_hint = _matches_any(blob, _VERIFIABLE_PATTERNS)
|
||||||
|
is_process = _matches_any(blob, _PROCESS_INTERNAL_PATTERNS)
|
||||||
|
is_doc = _matches_any(blob, _DOC_INTERNAL_PATTERNS)
|
||||||
|
|
||||||
|
# Wenn explicit Verifiable-Indikator + kein Process → verifiable
|
||||||
|
if is_verifiable_hint and not (is_process or is_doc):
|
||||||
|
return "verifiable"
|
||||||
|
# Wenn Process oder Doc UND nicht Verifiable → intern
|
||||||
|
if is_process and not is_verifiable_hint:
|
||||||
|
return "process_internal"
|
||||||
|
if is_doc and not is_verifiable_hint:
|
||||||
|
return "doc_internal"
|
||||||
|
# Beides → ambiguous, im Zweifel CHECK markieren
|
||||||
|
if is_process or is_doc:
|
||||||
|
return "ambiguous"
|
||||||
|
return "verifiable"
|
||||||
|
|
||||||
|
|
||||||
|
def annotate_mc_results(check_results: list[dict]) -> list[dict]:
|
||||||
|
"""In-place: setzt mc_audit_type auf jeden MC-Check und ersetzt
|
||||||
|
Status 'failed' durch 'check' wenn audit_type != verifiable."""
|
||||||
|
if not check_results:
|
||||||
|
return check_results
|
||||||
|
n_reclassified = 0
|
||||||
|
for r in check_results:
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
continue
|
||||||
|
if not (r.get("id") or "").startswith("mc-"):
|
||||||
|
continue
|
||||||
|
if "mc_audit_type" not in r:
|
||||||
|
r["mc_audit_type"] = classify_mc_audit_type(
|
||||||
|
r.get("label"), r.get("hint"), r.get("fail_criteria"),
|
||||||
|
)
|
||||||
|
# Wenn FAIL aber audit_type != verifiable → "check" (manuell)
|
||||||
|
if (not r.get("passed")
|
||||||
|
and not r.get("skipped")
|
||||||
|
and r["mc_audit_type"] in (
|
||||||
|
"process_internal", "doc_internal", "ambiguous",
|
||||||
|
)):
|
||||||
|
r["audit_status"] = "check" # NICHT failed
|
||||||
|
n_reclassified += 1
|
||||||
|
elif r.get("passed"):
|
||||||
|
r["audit_status"] = "pass"
|
||||||
|
elif r.get("skipped"):
|
||||||
|
r["audit_status"] = "skip"
|
||||||
|
else:
|
||||||
|
r["audit_status"] = "fail"
|
||||||
|
if n_reclassified:
|
||||||
|
logger.info(
|
||||||
|
"MC-Audit-Type: %d/%d MCs reklassifiziert von FAIL → CHECK "
|
||||||
|
"(interne Pruefung erforderlich)",
|
||||||
|
n_reclassified, len(check_results),
|
||||||
|
)
|
||||||
|
return check_results
|
||||||
|
|
||||||
|
|
||||||
|
def split_by_audit_type(check_results: list[dict]) -> dict[str, list[dict]]:
|
||||||
|
"""Liefert {verifiable_fails, internal_checks, passes, skips}."""
|
||||||
|
out = {"verifiable_fails": [], "internal_checks": [],
|
||||||
|
"passes": [], "skips": []}
|
||||||
|
for r in (check_results or []):
|
||||||
|
if not isinstance(r, dict):
|
||||||
|
continue
|
||||||
|
if not (r.get("id") or "").startswith("mc-"):
|
||||||
|
continue
|
||||||
|
status = r.get("audit_status")
|
||||||
|
if status == "pass":
|
||||||
|
out["passes"].append(r)
|
||||||
|
elif status == "skip":
|
||||||
|
out["skips"].append(r)
|
||||||
|
elif status == "check":
|
||||||
|
out["internal_checks"].append(r)
|
||||||
|
elif status == "fail" or (not r.get("passed") and not r.get("skipped")):
|
||||||
|
out["verifiable_fails"].append(r)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def build_internal_checks_block_html(
|
||||||
|
internal_checks: list[dict],
|
||||||
|
limit: int = 30,
|
||||||
|
) -> str:
|
||||||
|
if not internal_checks:
|
||||||
|
return ""
|
||||||
|
by_type: dict[str, list[dict]] = {}
|
||||||
|
for c in internal_checks:
|
||||||
|
t = c.get("mc_audit_type", "ambiguous")
|
||||||
|
by_type.setdefault(t, []).append(c)
|
||||||
|
|
||||||
|
sections: list[str] = []
|
||||||
|
labels = {
|
||||||
|
"process_internal": ("Interne Prozesse — bitte beim DSB pruefen",
|
||||||
|
"#1e40af"),
|
||||||
|
"doc_internal": ("Interne Dokumentation — bitte im VVT/DSFA pruefen",
|
||||||
|
"#5b21b6"),
|
||||||
|
"ambiguous": ("Unklar ob Audit-Befund oder interne Pruefung",
|
||||||
|
"#92400e"),
|
||||||
|
}
|
||||||
|
for atype, (heading, color) in labels.items():
|
||||||
|
items = by_type.get(atype) or []
|
||||||
|
if not items:
|
||||||
|
continue
|
||||||
|
rows = "".join(
|
||||||
|
f'<li style="margin-bottom:4px;font-size:11px;line-height:1.45">'
|
||||||
|
f'<strong>{(c.get("label") or "")[:160]}</strong>'
|
||||||
|
+ (f' <span style="color:#94a3b8">({c.get("regulation") or "—"})</span>'
|
||||||
|
if c.get("regulation") else '') +
|
||||||
|
f'</li>'
|
||||||
|
for c in items[:limit]
|
||||||
|
)
|
||||||
|
sections.append(
|
||||||
|
f'<div style="margin-bottom:10px">'
|
||||||
|
f'<div style="font-size:11px;color:{color};text-transform:uppercase;'
|
||||||
|
f'letter-spacing:1px;font-weight:600;margin-bottom:4px">'
|
||||||
|
f'{heading} ({len(items)})</div>'
|
||||||
|
f'<ul style="margin:0 0 0 18px;padding:0">{rows}</ul>'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
'<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;'
|
||||||
|
'max-width:760px;margin:0 auto 16px;padding:12px 16px;'
|
||||||
|
'background:#f0f9ff;border:1px solid #bfdbfe;border-radius:8px">'
|
||||||
|
'<div style="font-size:11px;color:#1e40af;text-transform:uppercase;'
|
||||||
|
'letter-spacing:1.2px;margin-bottom:4px;font-weight:600">'
|
||||||
|
'Pruefungen die wir von aussen NICHT durchfuehren koennen</div>'
|
||||||
|
f'<h3 style="margin:0 0 6px;font-size:14px;color:#1e293b">'
|
||||||
|
f'{len(internal_checks)} Pruefpunkt'
|
||||||
|
f'{"e" if len(internal_checks) != 1 else ""} sind '
|
||||||
|
'NUR intern beim Kunden zu pruefen</h3>'
|
||||||
|
'<p style="margin:0 0 10px;font-size:11px;color:#475569;'
|
||||||
|
'line-height:1.5">'
|
||||||
|
'Diese Anforderungen koennen wir per externem Website-Audit nicht '
|
||||||
|
'als erfuellt oder nicht-erfuellt bewerten — sie betreffen interne '
|
||||||
|
'Prozesse (Schulungen, AVV-Vertraege, TOM-Doku) oder interne '
|
||||||
|
'Dokumentation (VVT, DSFA, Loeschkonzept). Sie sind also <strong>kein '
|
||||||
|
'Verstoss</strong>, sondern Hinweis-Checks fuer Ihren DSB.</p>'
|
||||||
|
+ "".join(sections) +
|
||||||
|
'</div>'
|
||||||
|
)
|
||||||
@@ -61,6 +61,12 @@ def build_scorecard(check_results: list[dict]) -> dict:
|
|||||||
b["skipped"] += 1
|
b["skipped"] += 1
|
||||||
elif r.get("passed"):
|
elif r.get("passed"):
|
||||||
b["passed"] += 1
|
b["passed"] += 1
|
||||||
|
# P106 — interner Check ist KEIN Fail (zaehlt als skipped fuer
|
||||||
|
# die Score-Berechnung damit der Score realistisch ist).
|
||||||
|
elif r.get("audit_status") == "check":
|
||||||
|
b["skipped"] += 1
|
||||||
|
b.setdefault("internal_checks", 0)
|
||||||
|
b["internal_checks"] += 1
|
||||||
else:
|
else:
|
||||||
b["failed"] += 1
|
b["failed"] += 1
|
||||||
sev = (r.get("severity") or "MEDIUM").upper()
|
sev = (r.get("severity") or "MEDIUM").upper()
|
||||||
|
|||||||
@@ -28,4 +28,8 @@ USER appuser
|
|||||||
|
|
||||||
EXPOSE 8094
|
EXPOSE 8094
|
||||||
|
|
||||||
|
# P83 — Build-SHA fuer check-rebuild-needed.sh
|
||||||
|
ARG BUILD_SHA="unknown"
|
||||||
|
ENV BUILD_SHA=${BUILD_SHA}
|
||||||
|
|
||||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8094"]
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8094"]
|
||||||
|
|||||||
Reference in New Issue
Block a user