feat(iace): add hazard-matching-engine with component library, tag system, and pattern engine
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 44s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 4s
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 44s
CI/CD / test-python-backend-compliance (push) Successful in 33s
CI/CD / test-python-document-crawler (push) Successful in 22s
CI/CD / test-python-dsms-gateway (push) Successful in 19s
CI/CD / validate-canonical-controls (push) Successful in 13s
CI/CD / Deploy (push) Successful in 4s
Implements Phases 1-4 of the IACE Hazard-Matching-Engine: - 120 machine components (C001-C120) in 11 categories - 20 energy sources (EN01-EN20) - ~85 tag taxonomy across 5 domains - 44 hazard patterns with AND/NOT matching logic - Pattern engine with tag resolution and confidence scoring - 8 new API endpoints (component-library, energy-sources, tags, patterns, match/apply) - Completeness gate G09 for pattern matching - 320 tests passing (36 new) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -223,13 +223,13 @@ def _classify_regulation(regulation_code: str) -> dict:
|
||||
DOMAIN_KEYWORDS = {
|
||||
"AUTH": ["authentication", "login", "password", "credential", "mfa", "2fa",
|
||||
"session", "token", "oauth", "identity", "authentifizierung", "anmeldung"],
|
||||
"CRYPT": ["encryption", "cryptography", "tls", "ssl", "certificate", "hashing",
|
||||
"aes", "rsa", "verschlüsselung", "kryptographie", "zertifikat"],
|
||||
"CRYP": ["encryption", "cryptography", "tls", "ssl", "certificate", "hashing",
|
||||
"aes", "rsa", "verschlüsselung", "kryptographie", "cipher", "schlüssel"],
|
||||
"NET": ["network", "firewall", "dns", "vpn", "proxy", "segmentation",
|
||||
"netzwerk", "routing", "port", "intrusion"],
|
||||
"DATA": ["data protection", "privacy", "personal data", "datenschutz",
|
||||
"personenbezogen", "dsgvo", "gdpr", "löschung", "verarbeitung"],
|
||||
"LOG": ["logging", "monitoring", "audit", "siem", "alert", "anomaly",
|
||||
"LOG": ["logging", "monitoring", "audit trail", "siem", "alert", "anomaly",
|
||||
"protokollierung", "überwachung"],
|
||||
"ACC": ["access control", "authorization", "rbac", "permission", "privilege",
|
||||
"zugriffskontrolle", "berechtigung", "autorisierung"],
|
||||
@@ -241,12 +241,30 @@ DOMAIN_KEYWORDS = {
|
||||
"ki", "künstliche intelligenz", "algorithmus", "training"],
|
||||
"COMP": ["compliance", "audit", "regulation", "standard", "certification",
|
||||
"konformität", "prüfung", "zertifizierung"],
|
||||
"GOV": ["behörde", "verwaltung", "öffentlich", "register", "gewerberegister",
|
||||
"handelsregister", "meldepflicht", "aufsicht", "genehmigung", "bescheid",
|
||||
"verwaltungsakt", "ordnungswidrig", "bußgeld", "staat", "ministerium",
|
||||
"bundesamt", "landesamt", "kommune", "gebietskörperschaft"],
|
||||
"LAB": ["arbeitnehmer", "arbeitgeber", "arbeitsschutz", "arbeitszeit", "betriebsrat",
|
||||
"kündigung", "beschäftigung", "mindestlohn", "arbeitsvertrag", "betriebsverfassung",
|
||||
"arbeitsrecht", "arbeitsstätte", "gefährdungsbeurteilung", "unterweisung"],
|
||||
"FIN": ["finanz", "bankwesen", "zahlungsverkehr", "geldwäsche", "bilanz", "rechnungslegung",
|
||||
"buchführung", "jahresabschluss", "steuererklärung", "kapitalmarkt", "wertpapier",
|
||||
"kreditinstitut", "finanzdienstleistung", "bankenaufsicht", "bafin"],
|
||||
"TRD": ["handelsrecht", "gewerbeordnung", "gewerbe", "handwerk", "gewerbeuntersagung",
|
||||
"gewerbebetrieb", "handelsgesetzbuch", "handelsregister", "kaufmann",
|
||||
"unternehmer", "wettbewerb", "verbraucherschutz", "produktsicherheit"],
|
||||
"ENV": ["umweltschutz", "emission", "abfall", "immission", "gewässerschutz",
|
||||
"naturschutz", "umweltverträglichkeit", "klimaschutz", "nachhaltigkeit",
|
||||
"entsorgung", "recycling", "umweltrecht"],
|
||||
"HLT": ["gesundheit", "medizinprodukt", "arzneimittel", "patient", "krankenhaus",
|
||||
"hygiene", "infektionsschutz", "medizin", "pflege", "therapie"],
|
||||
}
|
||||
|
||||
|
||||
CATEGORY_KEYWORDS = {
|
||||
"encryption": ["encryption", "cryptography", "tls", "ssl", "certificate", "hashing",
|
||||
"aes", "rsa", "verschlüsselung", "kryptographie", "zertifikat", "cipher"],
|
||||
"aes", "rsa", "verschlüsselung", "kryptographie", "cipher", "schlüssel"],
|
||||
"authentication": ["authentication", "login", "password", "credential", "mfa", "2fa",
|
||||
"session", "oauth", "authentifizierung", "anmeldung", "passwort"],
|
||||
"network": ["network", "firewall", "dns", "vpn", "proxy", "segmentation",
|
||||
@@ -278,6 +296,20 @@ CATEGORY_KEYWORDS = {
|
||||
"plattform", "geräte"],
|
||||
"identity": ["identity", "iam", "directory", "ldap", "sso", "provisioning",
|
||||
"identität", "identitätsmanagement", "benutzerverzeichnis"],
|
||||
"public_administration": ["behörde", "verwaltung", "öffentlich", "register", "gewerberegister",
|
||||
"handelsregister", "meldepflicht", "aufsicht", "genehmigung", "bescheid",
|
||||
"verwaltungsakt", "ordnungswidrig", "bußgeld", "amt"],
|
||||
"labor_law": ["arbeitnehmer", "arbeitgeber", "arbeitsschutz", "arbeitszeit", "betriebsrat",
|
||||
"kündigung", "beschäftigung", "mindestlohn", "arbeitsvertrag", "betriebsverfassung"],
|
||||
"finance": ["finanz", "bankwesen", "zahlungsverkehr", "geldwäsche", "bilanz", "rechnungslegung",
|
||||
"buchführung", "jahresabschluss", "kapitalmarkt", "wertpapier", "bafin"],
|
||||
"trade_regulation": ["gewerbeordnung", "gewerbe", "handwerk", "gewerbeuntersagung",
|
||||
"gewerbebetrieb", "handelsrecht", "kaufmann", "wettbewerb",
|
||||
"verbraucherschutz", "produktsicherheit"],
|
||||
"environmental": ["umweltschutz", "emission", "abfall", "immission", "gewässerschutz",
|
||||
"naturschutz", "klimaschutz", "nachhaltigkeit", "entsorgung"],
|
||||
"health": ["gesundheit", "medizinprodukt", "arzneimittel", "patient", "krankenhaus",
|
||||
"hygiene", "infektionsschutz", "pflege"],
|
||||
}
|
||||
|
||||
VERIFICATION_KEYWORDS = {
|
||||
@@ -372,7 +404,8 @@ class GeneratedControl:
|
||||
generation_strategy: str = "ungrouped" # ungrouped | document_grouped
|
||||
# Classification fields
|
||||
verification_method: Optional[str] = None # code_review, document, tool, hybrid
|
||||
category: Optional[str] = None # one of 17 categories
|
||||
category: Optional[str] = None # one of 22 categories
|
||||
target_audience: Optional[list] = None # e.g. ["unternehmen", "behoerden", "entwickler"]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -705,9 +738,11 @@ class ControlGeneratorPipeline:
|
||||
page = 0
|
||||
collection_total = 0
|
||||
collection_new = 0
|
||||
seen_offsets: set[str] = set() # Detect scroll loops
|
||||
max_pages = 1000 # Safety limit: 1000 pages × 200 = 200K chunks max per collection
|
||||
prev_chunk_count = -1 # Track stalls (same count means no progress)
|
||||
stall_count = 0
|
||||
|
||||
while True:
|
||||
while page < max_pages:
|
||||
chunks, next_offset = await self.rag.scroll(
|
||||
collection=collection,
|
||||
offset=offset,
|
||||
@@ -747,17 +782,30 @@ class ControlGeneratorPipeline:
|
||||
# Stop conditions
|
||||
if not next_offset:
|
||||
break
|
||||
# Detect infinite scroll loops (Qdrant mixed ID types)
|
||||
if next_offset in seen_offsets:
|
||||
logger.warning(
|
||||
"Scroll loop detected in %s at offset %s (page %d) — stopping",
|
||||
collection, next_offset, page,
|
||||
)
|
||||
break
|
||||
seen_offsets.add(next_offset)
|
||||
|
||||
# Detect stalls: if no NEW unique chunks found for several pages,
|
||||
# we've likely cycled through all chunks in this collection.
|
||||
# (Safer than offset dedup which breaks with mixed Qdrant ID types)
|
||||
if collection_new == prev_chunk_count:
|
||||
stall_count += 1
|
||||
if stall_count >= 5:
|
||||
logger.warning(
|
||||
"Scroll stalled in %s at page %d — no new unique chunks for 5 pages (%d total, %d new) — stopping",
|
||||
collection, page, collection_total, collection_new,
|
||||
)
|
||||
break
|
||||
else:
|
||||
stall_count = 0
|
||||
prev_chunk_count = collection_new
|
||||
|
||||
offset = next_offset
|
||||
|
||||
if page >= max_pages:
|
||||
logger.warning(
|
||||
"Collection %s: reached max_pages limit (%d). %d chunks scrolled.",
|
||||
collection, max_pages, collection_total,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Collection %s: %d total chunks scrolled, %d new unprocessed",
|
||||
collection, collection_total, collection_new,
|
||||
@@ -823,7 +871,9 @@ Quelle: {chunk.regulation_name} ({chunk.regulation_code}), {chunk.article}"""
|
||||
control.license_rule = 1
|
||||
control.source_original_text = chunk.text
|
||||
control.source_citation = {
|
||||
"source": f"{chunk.regulation_name} {chunk.article or ''}".strip(),
|
||||
"source": chunk.regulation_name,
|
||||
"article": chunk.article or "",
|
||||
"paragraph": chunk.paragraph or "",
|
||||
"license": license_info.get("license", ""),
|
||||
"url": chunk.source_url or "",
|
||||
}
|
||||
@@ -835,6 +885,7 @@ Quelle: {chunk.regulation_name} ({chunk.regulation_code}), {chunk.article}"""
|
||||
"license_rule": 1,
|
||||
"source_regulation": chunk.regulation_code,
|
||||
"source_article": chunk.article,
|
||||
"source_paragraph": chunk.paragraph,
|
||||
}
|
||||
return control
|
||||
|
||||
@@ -873,7 +924,9 @@ Quelle: {chunk.regulation_name}, {chunk.article}"""
|
||||
control.license_rule = 2
|
||||
control.source_original_text = chunk.text
|
||||
control.source_citation = {
|
||||
"source": f"{chunk.regulation_name} {chunk.article or ''}".strip(),
|
||||
"source": chunk.regulation_name,
|
||||
"article": chunk.article or "",
|
||||
"paragraph": chunk.paragraph or "",
|
||||
"license": license_info.get("license", ""),
|
||||
"license_notice": attribution,
|
||||
"url": chunk.source_url or "",
|
||||
@@ -886,6 +939,7 @@ Quelle: {chunk.regulation_name}, {chunk.article}"""
|
||||
"license_rule": 2,
|
||||
"source_regulation": chunk.regulation_code,
|
||||
"source_article": chunk.article,
|
||||
"source_paragraph": chunk.paragraph,
|
||||
}
|
||||
return control
|
||||
|
||||
@@ -913,7 +967,9 @@ Gib JSON zurück mit diesen Feldern:
|
||||
- test_procedure: Liste von Prüfschritten (Strings)
|
||||
- evidence: Liste von Nachweisdokumenten (Strings)
|
||||
- severity: low/medium/high/critical
|
||||
- tags: Liste von Tags (eigene Begriffe)"""
|
||||
- tags: Liste von Tags (eigene Begriffe)
|
||||
- domain: Fachgebiet als Kuerzel (AUTH, CRYP, NET, DATA, LOG, ACC, SEC, INC, AI, COMP, GOV, LAB, FIN, TRD, ENV, HLT)
|
||||
- target_audience: Liste der Zielgruppen (z.B. "unternehmen", "behoerden", "entwickler", "datenschutzbeauftragte", "geschaeftsfuehrung", "it-abteilung", "rechtsabteilung", "compliance-officer", "personalwesen", "oeffentlicher_dienst")"""
|
||||
|
||||
raw = await _llm_chat(prompt, REFORM_SYSTEM_PROMPT)
|
||||
data = _parse_llm_json(raw)
|
||||
@@ -989,6 +1045,8 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
|
||||
- evidence: Liste von Nachweisdokumenten (Strings, Deutsch)
|
||||
- severity: low/medium/high/critical
|
||||
- tags: Liste von Tags
|
||||
- domain: Fachgebiet als Kuerzel (AUTH=Authentifizierung, CRYP=Kryptographie, NET=Netzwerk, DATA=Datenschutz, LOG=Logging, ACC=Zugriffskontrolle, SEC=IT-Sicherheit, INC=Vorfallmanagement, AI=KI, COMP=Compliance, GOV=Behoerden/Verwaltung, LAB=Arbeitsrecht, FIN=Finanzregulierung, TRD=Gewerbe/Handelsrecht, ENV=Umwelt, HLT=Gesundheit)
|
||||
- target_audience: Liste der Zielgruppen fuer die dieses Control relevant ist. Moegliche Werte: "unternehmen", "behoerden", "entwickler", "datenschutzbeauftragte", "geschaeftsfuehrung", "it-abteilung", "rechtsabteilung", "compliance-officer", "personalwesen", "einkauf", "produktion", "vertrieb", "gesundheitswesen", "finanzwesen", "oeffentlicher_dienst"
|
||||
|
||||
{joined}"""
|
||||
|
||||
@@ -1016,7 +1074,9 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
|
||||
if lic["rule"] in (1, 2):
|
||||
control.source_original_text = chunk.text
|
||||
control.source_citation = {
|
||||
"source": f"{chunk.regulation_name} {chunk.article or ''}".strip(),
|
||||
"source": chunk.regulation_name,
|
||||
"article": chunk.article or "",
|
||||
"paragraph": chunk.paragraph or "",
|
||||
"license": lic.get("license", ""),
|
||||
"license_notice": lic.get("attribution", ""),
|
||||
"url": chunk.source_url or "",
|
||||
@@ -1030,6 +1090,7 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
|
||||
"license_rule": lic["rule"],
|
||||
"source_regulation": chunk.regulation_code,
|
||||
"source_article": chunk.article,
|
||||
"source_paragraph": chunk.paragraph,
|
||||
"batch_size": len(chunks),
|
||||
"document_grouped": same_doc,
|
||||
}
|
||||
@@ -1071,6 +1132,8 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
|
||||
- evidence: Liste von Nachweisdokumenten (Strings)
|
||||
- severity: low/medium/high/critical
|
||||
- tags: Liste von Tags (eigene Begriffe)
|
||||
- domain: Fachgebiet als Kuerzel (AUTH=Authentifizierung, CRYP=Kryptographie, NET=Netzwerk, DATA=Datenschutz, LOG=Logging, ACC=Zugriffskontrolle, SEC=IT-Sicherheit, INC=Vorfallmanagement, AI=KI, COMP=Compliance, GOV=Behoerden/Verwaltung, LAB=Arbeitsrecht, FIN=Finanzregulierung, TRD=Gewerbe/Handelsrecht, ENV=Umwelt, HLT=Gesundheit)
|
||||
- target_audience: Liste der Zielgruppen (z.B. "unternehmen", "behoerden", "entwickler", "datenschutzbeauftragte", "geschaeftsfuehrung", "it-abteilung", "rechtsabteilung", "compliance-officer", "personalwesen", "einkauf", "produktion", "gesundheitswesen", "finanzwesen", "oeffentlicher_dienst")
|
||||
|
||||
{joined}"""
|
||||
|
||||
@@ -1182,8 +1245,10 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
|
||||
else:
|
||||
control.release_state = "needs_review"
|
||||
|
||||
# Control ID
|
||||
domain = config.domain or _detect_domain(control.objective)
|
||||
# Control ID — prefer LLM-assigned domain over keyword detection
|
||||
domain = (control.generation_metadata.get("_effective_domain")
|
||||
or config.domain
|
||||
or _detect_domain(control.objective))
|
||||
control.control_id = self._generate_control_id(domain, self.db)
|
||||
control.generation_metadata["job_id"] = job_id
|
||||
|
||||
@@ -1270,7 +1335,21 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
|
||||
if isinstance(tags, str):
|
||||
tags = [t.strip() for t in tags.split(",")]
|
||||
|
||||
return GeneratedControl(
|
||||
# Use LLM-provided domain if available, fallback to keyword-detected domain
|
||||
llm_domain = data.get("domain")
|
||||
valid_domains = {"AUTH", "CRYP", "NET", "DATA", "LOG", "ACC", "SEC", "INC",
|
||||
"AI", "COMP", "GOV", "LAB", "FIN", "TRD", "ENV", "HLT"}
|
||||
if llm_domain and llm_domain.upper() in valid_domains:
|
||||
domain = llm_domain.upper()
|
||||
|
||||
# Parse target_audience from LLM response
|
||||
target_audience = data.get("target_audience")
|
||||
if isinstance(target_audience, str):
|
||||
target_audience = [t.strip() for t in target_audience.split(",")]
|
||||
if not isinstance(target_audience, list):
|
||||
target_audience = None
|
||||
|
||||
control = GeneratedControl(
|
||||
title=str(data.get("title", "Untitled Control"))[:255],
|
||||
objective=str(data.get("objective", "")),
|
||||
rationale=str(data.get("rationale", "")),
|
||||
@@ -1282,7 +1361,11 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
|
||||
risk_score=min(10.0, max(0.0, float(data.get("risk_score", 5.0)))),
|
||||
implementation_effort=data.get("implementation_effort", "m") if data.get("implementation_effort") in ("s", "m", "l", "xl") else "m",
|
||||
tags=tags[:20],
|
||||
target_audience=target_audience,
|
||||
)
|
||||
# Store effective domain for later control_id generation
|
||||
control.generation_metadata["_effective_domain"] = domain
|
||||
return control
|
||||
|
||||
def _fallback_control(self, chunk: RAGSearchResult) -> GeneratedControl:
|
||||
"""Create a minimal control when LLM parsing fails."""
|
||||
@@ -1393,7 +1476,8 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
|
||||
open_anchors, release_state, tags,
|
||||
license_rule, source_original_text, source_citation,
|
||||
customer_visible, generation_metadata,
|
||||
verification_method, category, generation_strategy
|
||||
verification_method, category, generation_strategy,
|
||||
target_audience
|
||||
) VALUES (
|
||||
:framework_id, :control_id, :title, :objective, :rationale,
|
||||
:scope, :requirements, :test_procedure, :evidence,
|
||||
@@ -1401,7 +1485,8 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
|
||||
:open_anchors, :release_state, :tags,
|
||||
:license_rule, :source_original_text, :source_citation,
|
||||
:customer_visible, :generation_metadata,
|
||||
:verification_method, :category, :generation_strategy
|
||||
:verification_method, :category, :generation_strategy,
|
||||
:target_audience
|
||||
)
|
||||
ON CONFLICT (framework_id, control_id) DO NOTHING
|
||||
RETURNING id
|
||||
@@ -1430,6 +1515,7 @@ Gib ein JSON-Array zurueck mit GENAU {len(chunks)} Objekten. Jedes Objekt hat di
|
||||
"verification_method": control.verification_method,
|
||||
"category": control.category,
|
||||
"generation_strategy": control.generation_strategy,
|
||||
"target_audience": json.dumps(control.target_audience) if control.target_audience else None,
|
||||
},
|
||||
)
|
||||
self.db.commit()
|
||||
|
||||
Reference in New Issue
Block a user