feat(templates,campaigns): validate MITRE technique IDs, surface template suggestions first, manager can re-approve rejected campaigns
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
- TestTemplate/TemplateSuggestion creation and updates now reject any mitre_technique_id that doesn't match a real, already-synced MITRE ATT&CK technique. Frontend swaps the free-text ID input for a technique picker. - Test Catalog now shows pending template suggestions before the catalog grid, with full field detail (platform, severity, tool, atomic ID, source URL, remediation) instead of just name/procedure. - A manager can edit and directly re-approve a campaign they previously rejected, instead of needing the original lead to resubmit it.
This commit is contained in:
@@ -12,7 +12,7 @@ from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Import EntityNotFoundError from app.domain.errors
|
||||
from app.domain.errors import EntityNotFoundError
|
||||
from app.domain.errors import BusinessRuleViolation, EntityNotFoundError
|
||||
|
||||
# Import TestTemplate from app.models.test_template
|
||||
from app.models.test_template import TestTemplate
|
||||
@@ -235,9 +235,22 @@ def get_template_or_raise(db: Session, template_id: uuid.UUID) -> TestTemplate:
|
||||
return template
|
||||
|
||||
|
||||
def validate_mitre_technique_id(db: Session, mitre_technique_id: str) -> None:
|
||||
"""Raise BusinessRuleViolation unless *mitre_technique_id* matches a real,
|
||||
already-synced MITRE ATT&CK technique — free text like "made up" or a
|
||||
typo'd ID would otherwise silently create an orphaned template that can
|
||||
never be found via technique lookups or coverage reporting."""
|
||||
exists = db.query(Technique).filter(Technique.mitre_id == mitre_technique_id).first()
|
||||
if not exists:
|
||||
raise BusinessRuleViolation(
|
||||
f"'{mitre_technique_id}' is not a known MITRE ATT&CK technique ID"
|
||||
)
|
||||
|
||||
|
||||
# Define function create_template
|
||||
def create_template(db: Session, **fields: object) -> TestTemplate:
|
||||
"""Create a test template from keyword args (e.g. payload.model_dump()). Does NOT commit."""
|
||||
validate_mitre_technique_id(db, fields["mitre_technique_id"])
|
||||
# Assign template = TestTemplate(**fields)
|
||||
template = TestTemplate(**fields)
|
||||
# Stage new record(s) for database insertion
|
||||
@@ -249,6 +262,8 @@ def create_template(db: Session, **fields: object) -> TestTemplate:
|
||||
# Define function update_template
|
||||
def update_template(db: Session, template_id: uuid.UUID, **fields: object) -> TestTemplate:
|
||||
"""Update an existing template. Raises EntityNotFoundError if not found. Does NOT commit."""
|
||||
if fields.get("mitre_technique_id"):
|
||||
validate_mitre_technique_id(db, fields["mitre_technique_id"])
|
||||
# Assign template = get_template_or_raise(db, template_id)
|
||||
template = get_template_or_raise(db, template_id)
|
||||
# Iterate over fields.items()
|
||||
|
||||
Reference in New Issue
Block a user