From a58f9fd357b4e11aacd2a7f8a854d5ec6b2bebfa Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 18 Jun 2026 16:26:11 +0200 Subject: [PATCH] feat(techniques): add external_references storage and display MITRE sources with links --- .../b049_add_technique_external_references.py | 26 +++++++++ backend/app/models/technique.py | 2 + backend/app/services/mitre_sync_service.py | 6 +++ .../app/services/technique_query_service.py | 1 + frontend/src/pages/TechniqueDetailPage.tsx | 53 +++++++++++++++++++ frontend/src/types/models.ts | 8 +++ 6 files changed, 96 insertions(+) create mode 100644 backend/alembic/versions/b049_add_technique_external_references.py diff --git a/backend/alembic/versions/b049_add_technique_external_references.py b/backend/alembic/versions/b049_add_technique_external_references.py new file mode 100644 index 0000000..3bf9f7e --- /dev/null +++ b/backend/alembic/versions/b049_add_technique_external_references.py @@ -0,0 +1,26 @@ +"""Add external_references column to techniques table. + +Revision ID: b049 +Revises: b048 +Create Date: 2026-06-18 +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSONB + +revision = "b049" +down_revision = "b048" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "techniques", + sa.Column("external_references", JSONB, nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("techniques", "external_references") diff --git a/backend/app/models/technique.py b/backend/app/models/technique.py index 0e405b7..6b21142 100644 --- a/backend/app/models/technique.py +++ b/backend/app/models/technique.py @@ -59,6 +59,8 @@ class Technique(Base): review_required = Column(Boolean, default=False) # Assign last_review_date = Column(DateTime, nullable=True) last_review_date = Column(DateTime, nullable=True) + # Assign external_references = Column(JSONB, nullable=True, default=list) + external_references = Column(JSONB, nullable=True, default=list) # Relationships tests = relationship("Test", back_populates="technique") diff --git a/backend/app/services/mitre_sync_service.py b/backend/app/services/mitre_sync_service.py index aa4fe98..e78cf0e 100644 --- a/backend/app/services/mitre_sync_service.py +++ b/backend/app/services/mitre_sync_service.py @@ -263,6 +263,8 @@ def sync_mitre(db: Session) -> dict: is_subtechnique = "." in mitre_id # Assign parent_mitre_id = mitre_id.split(".")[0] if is_subtechnique else None parent_mitre_id = mitre_id.split(".")[0] if is_subtechnique else None + # Assign external_references = obj.get("external_references", []) + external_references = obj.get("external_references", []) # Assign existing = existing_techniques.get(mitre_id) existing = existing_techniques.get(mitre_id) @@ -289,6 +291,8 @@ def sync_mitre(db: Session) -> dict: is_subtechnique=is_subtechnique, # Keyword argument: parent_mitre_id parent_mitre_id=parent_mitre_id, + # Keyword argument: external_references + external_references=external_references, # Keyword argument: status_global status_global=TechniqueStatus.not_evaluated, # Keyword argument: review_required @@ -331,6 +335,8 @@ def sync_mitre(db: Session) -> dict: existing.is_subtechnique = is_subtechnique # Assign existing.parent_mitre_id = parent_mitre_id existing.parent_mitre_id = parent_mitre_id + # Assign existing.external_references = external_references + existing.external_references = external_references # Check: changes if changes: diff --git a/backend/app/services/technique_query_service.py b/backend/app/services/technique_query_service.py index 9e8c4d1..3eb3367 100644 --- a/backend/app/services/technique_query_service.py +++ b/backend/app/services/technique_query_service.py @@ -131,4 +131,5 @@ def get_technique_detail(db: Session, mitre_id: str) -> dict: } for item in intel_items ], + "external_references": technique.external_references or [], } diff --git a/frontend/src/pages/TechniqueDetailPage.tsx b/frontend/src/pages/TechniqueDetailPage.tsx index c4125bc..7f587cd 100644 --- a/frontend/src/pages/TechniqueDetailPage.tsx +++ b/frontend/src/pages/TechniqueDetailPage.tsx @@ -151,6 +151,12 @@ function TechniqueJiraSection({ technique }: { technique: ReturnType): string { + const ref = externalRefs?.find((r) => r.source_name === "mitre-attack" && r.url); + if (ref?.url) return ref.url; + return `https://attack.mitre.org/techniques/${mitreId.replace(".", "/")}/`; +} + export default function TechniqueDetailPage() { const { mitreId } = useParams<{ mitreId: string }>(); const navigate = useNavigate(); @@ -349,6 +355,20 @@ export default function TechniqueDetailPage() {
MITRE Version
{technique.mitre_version || "—"}
+
+
MITRE ATT&CK
+
+ + + View on MITRE ATT&CK + +
+
@@ -655,6 +675,39 @@ export default function TechniqueDetailPage() { )} + {/* External References */} + {technique.external_references && technique.external_references.filter((r) => r.source_name !== "mitre-attack" && r.url).length > 0 && ( +
+

References

+
+ {technique.external_references + .filter((r) => r.source_name !== "mitre-attack" && r.url) + .map((ref, i) => ( +
+
+

{ref.source_name}

+ {ref.description && ( +

{ref.description}

+ )} +
+ + View + + +
+ ))} +
+
+ )} + {/* Jira Integration — shows tickets linked to tests of this technique */} {technique && } diff --git a/frontend/src/types/models.ts b/frontend/src/types/models.ts index 302c3f0..daf2f9f 100644 --- a/frontend/src/types/models.ts +++ b/frontend/src/types/models.ts @@ -11,6 +11,13 @@ export interface User { // ── Techniques ───────────────────────────────────────────────────── +export interface TechniqueExternalReference { + source_name: string; + url?: string; + external_id?: string; + description?: string; +} + export interface Technique { id: string; mitre_id: string; @@ -25,6 +32,7 @@ export interface Technique { status_global: TechniqueStatus; review_required: boolean; last_review_date: string | null; + external_references?: TechniqueExternalReference[]; } export type TechniqueStatus =