fix: 4 improvements — campaign test deletion, review queue triggers, technique link, Jira read-only
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

1. Campaign test deletion: removing a test from a campaign now also
   deletes the underlying Test record and recalculates technique status.

2. Review Queue triggers: review_required=True is now also set when
   - Sigma/Elastic detection rules are imported for a technique
   - A test is validated (coverage status changes)

3. Test detail — Technique link: 'Technique' entry added at the top of
   the Details sidebar showing MITRE ID + name as a clickable link to
   /techniques/{mitre_id}.

4. Jira panel — read-only on test page: added readOnly + label props to
   JiraLinkPanel. TestDetailPage now passes readOnly=true and the test
   name as label, hiding Link Issue / Sync / Unlink controls (automatic
   Jira creation only — no manual management).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-29 11:18:55 +02:00
parent 727b8af7fd
commit 1b513b050e
6 changed files with 118 additions and 40 deletions

View File

@@ -320,9 +320,28 @@ def remove_test_from_campaign(db: Session, campaign_id: str, campaign_test_id: s
for dep in dependents:
dep.depends_on = None
# Keep a reference to the underlying test before deleting the join record
test_id = ct.test_id
technique_id = None
test_obj = db.query(Test).filter(Test.id == test_id).first()
if test_obj:
technique_id = test_obj.technique_id
db.delete(ct)
db.flush()
# Also delete the actual test record (it was created for this campaign)
if test_obj:
db.delete(test_obj)
db.flush()
# Recalculate technique status_global so coverage metrics stay consistent
if technique_id:
technique = db.query(Technique).filter(Technique.id == technique_id).first()
if technique:
recalculate_technique_status(db, technique)
db.flush()
def activate_campaign(db: Session, campaign_id: str) -> Campaign:
"""Activate a campaign, moving it from draft to active.

View File

@@ -34,6 +34,7 @@ from sqlalchemy.orm import Session
from app.models.detection_rule import DetectionRule
from app.models.data_source import DataSource
from app.models.technique import Technique
from app.services.audit_service import log_action
logger = logging.getLogger(__name__)
@@ -316,6 +317,7 @@ def sync(db: Session) -> dict:
created = 0
skipped = 0
new_technique_ids: set[str] = set()
for item in parsed_rules:
if item["source_id"] in existing_ids:
@@ -337,8 +339,15 @@ def sync(db: Session) -> dict:
)
db.add(rule)
existing_ids.add(item["source_id"])
new_technique_ids.add(item["mitre_technique_id"])
created += 1
# Flag techniques that received new rules for review
if new_technique_ids:
db.query(Technique).filter(
Technique.mitre_id.in_(new_technique_ids)
).update({"review_required": True}, synchronize_session=False)
db.commit()
summary = {

View File

@@ -37,6 +37,7 @@ from sqlalchemy.orm import Session
from app.models.detection_rule import DetectionRule
from app.models.data_source import DataSource
from app.models.technique import Technique
from app.services.audit_service import log_action
logger = logging.getLogger(__name__)
@@ -288,6 +289,7 @@ def sync(db: Session) -> dict:
created = 0
skipped = 0
new_technique_ids: set[str] = set()
for item in parsed_rules:
# Dedup key: source_id (relative path). A rule file may produce
@@ -316,8 +318,15 @@ def sync(db: Session) -> dict:
)
db.add(rule)
existing_ids.add(item["source_id"])
new_technique_ids.add(item["mitre_technique_id"])
created += 1
# Flag techniques that received new rules for review
if new_technique_ids:
db.query(Technique).filter(
Technique.mitre_id.in_(new_technique_ids)
).update({"review_required": True}, synchronize_session=False)
db.commit()
summary = {