Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VITE_API_URL=http://localhost:8000
VITE_SUPABASE_URL=https://<project>.supabase.co
VITE_SUPABASE_ANON_KEY=
23 changes: 23 additions & 0 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: frontend

on:
push:
paths:
- 'src/**'
- 'package.json'
- 'vite.config.ts'

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: |
if [ -f node_modules/.bin/eslint ]; then
npm run lint
fi
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,22 @@ catchattack/
npm run build
```

### Running Front-end

Create a `.env` file at project root with:

```bash
VITE_API_URL=http://localhost:8000
VITE_SUPABASE_URL=https://<project>.supabase.co
VITE_SUPABASE_ANON_KEY=
```

Then start the dev server:

```bash
npm run dev
```

### Start Backend API

The Python backend is located in `backend/`. Install dependencies and set up the environment file:
Expand Down
3 changes: 3 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
SUPABASE_URL=http://localhost:54321
SUPABASE_ANON_KEY=
CALDERA_URL=
CALDERA_USER=
CALDERA_PASS=
7 changes: 7 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ The emulator service requires the following variables:
```
SUPABASE_URL=http://localhost:54321
SUPABASE_ANON_KEY=<your-key>
CALDERA_URL=
CALDERA_USER=
CALDERA_PASS=
```

Add these variables to a `.env` file. An example template is provided in
Expand All @@ -29,6 +32,10 @@ uvicorn backend.main:app --reload
```
Visit `http://localhost:8000/docs` for interactive API documentation.

### New Endpoint

`GET /techniques/{id}/full` returns MITRE data combined with Atomic Red Team tests and CALDERA abilities.

## Directory Overview
```
backend/
Expand Down
30 changes: 22 additions & 8 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from . import schemas
from pathlib import Path as FilePath
from .services import emulator, mitre, sigma, yaml_generator, vm_manager
from .services import security_sources
from datetime import datetime

app = FastAPI(title="CatchAttack Backend")
Expand Down Expand Up @@ -63,27 +64,40 @@ def list_techniques():
return {"techniques": techniques}


@app.get("/techniques/{tech_id}/full")
def full_technique(tech_id: str = Path(..., min_length=1)):
data = security_sources.get_full_technique(tech_id)
if not data:
raise HTTPException(status_code=404, detail="Technique not found")
return data


@app.post("/sigma/{technique_id}")
def create_sigma(technique_id: str = Path(..., min_length=1)):
info = security_sources.get_full_technique(technique_id)
if not info:
raise HTTPException(status_code=404, detail="Technique not found")
try:
return sigma.generate_sigma_rule(technique_id)
rule = sigma.generate_sigma_rule(technique_id)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
rule.update({
"technique": info["technique"],
"atomic_tests": info["atomic_tests"],
"caldera_abilities": info["caldera_abilities"],
})
return rule


@app.post("/yaml/{technique_id}")
def generate_yaml(
config: schemas.VMConfig,
technique_id: str = Path(..., min_length=1),
):
try:
data = mitre.fetch_techniques()
except Exception as e:
raise HTTPException(status_code=502, detail=f"MITRE fetch failed: {e}")

technique = next((t for t in data if t.get("id") == technique_id), None)
if not technique:
info = security_sources.get_full_technique(technique_id)
if not info:
raise HTTPException(status_code=404, detail="Technique not found")
technique = info["technique"]

merged = {
"technique": {
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ stix2
taxii2-client
PyYAML
requests
requests-mock
49 changes: 49 additions & 0 deletions backend/services/art.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import json
import logging
import time
from pathlib import Path
from typing import List, Dict

import requests
import yaml

BASE_URL = "https://raw.githubusercontent.com/redcanaryco/atomic-red-team/master/atomics"
CACHE_DIR = Path("generated/atomic_tests")
CACHE_DIR.mkdir(parents=True, exist_ok=True)

_CACHE: Dict[str, List[dict]] = {}
_LAST_FETCH: Dict[str, float] = {}
_TTL = 60 * 60 * 24 # 24h


def get_atomic_tests(tech_id: str) -> List[dict]:
"""Return Atomic Red Team tests for a technique.

Tests are cached on disk for 24 hours.
"""
now = time.time()
if tech_id in _CACHE and now - _LAST_FETCH.get(tech_id, 0) < _TTL:
return _CACHE[tech_id]

url = f"{BASE_URL}/{tech_id}/{tech_id}.yaml"
cache_file = CACHE_DIR / f"{tech_id}.json"
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
data = yaml.safe_load(resp.text) or {}
tests = data.get("atomic_tests", [])
_CACHE[tech_id] = tests
_LAST_FETCH[tech_id] = now
cache_file.write_text(json.dumps(tests))
return tests
except Exception as exc: # noqa: BLE001
logging.exception("Atomic fetch failed: %s", exc)
if cache_file.exists():
try:
tests = json.loads(cache_file.read_text())
_CACHE[tech_id] = tests
_LAST_FETCH[tech_id] = now
return tests
except Exception: # noqa: BLE001
logging.exception("Failed to load cached atomic tests")
return []
32 changes: 32 additions & 0 deletions backend/services/caldera.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os
import requests
import logging
from typing import List, Dict, Any

TIMEOUT = 10


def _enabled() -> bool:
return all(
[os.getenv("CALDERA_URL"), os.getenv("CALDERA_USER"), os.getenv("CALDERA_PASS")]
)


def _request(path: str) -> Any:
base = os.getenv("CALDERA_URL", "")
user = os.getenv("CALDERA_USER", "")
pwd = os.getenv("CALDERA_PASS", "")
resp = requests.get(f"{base}{path}", auth=(user, pwd), timeout=TIMEOUT)
resp.raise_for_status()
return resp.json()


def get_abilities(tech_id: str) -> Any:
if not _enabled():
return {"status": "disabled"}
try:
data = _request(f"/api/v2/abilities?technique_id={tech_id}")
return data
except Exception as exc: # noqa: BLE001
logging.exception("CALDERA abilities error: %s", exc)
return []
30 changes: 30 additions & 0 deletions backend/services/security_sources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from typing import Optional, Dict, Any

from . import mitre
from . import art
from . import caldera


def get_techniques() -> list[dict]:
return mitre.fetch_techniques()


def get_full_technique(tech_id: str) -> Optional[Dict[str, Any]]:
techs = mitre.fetch_techniques()
technique = next((t for t in techs if t.get("id") == tech_id), None)
if not technique:
return None

atomic_tests = art.get_atomic_tests(tech_id)
caldera_abilities = caldera.get_abilities(tech_id)
return {
"technique": {
"id": tech_id,
"name": technique.get("name"),
"description": technique.get("description"),
"platforms": technique.get("x_mitre_platforms", []),
"data_sources": technique.get("x_mitre_data_sources", []),
},
"atomic_tests": atomic_tests,
"caldera_abilities": caldera_abilities,
}
6 changes: 5 additions & 1 deletion src/components/detection/CoverageTabContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import MitreMatrix from "@/components/mitre/MitreMatrix";
import { useQuery } from '@tanstack/react-query';
import { getTechniques } from '@/services/api';
import CoverageAnalysis from "@/components/detection/CoverageAnalysis";
import DetectionRules from "@/components/detection/DetectionRules";
import { EmulationResult } from "@/types/backend";
Expand Down Expand Up @@ -30,6 +32,7 @@ const CoverageTabContent = ({
selectedTechniques,
onTechniqueSelect
}: CoverageTabContentProps) => {
const { data } = useQuery(['techniques'], () => getTechniques().then(r => r.data));
return (
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
Expand All @@ -46,11 +49,12 @@ const CoverageTabContent = ({
<CardHeader>Visualization of covered techniques in the MITRE ATT&CK framework</CardHeader>
</CardHeader>
<CardContent>
<MitreMatrix
<MitreMatrix
selectedTechniques={selectedTechniques}
onTechniqueSelect={onTechniqueSelect}
coveredTechniques={coveredTechniques}
isInteractive={true}
techniques={data?.techniques}
/>
</CardContent>
</Card>
Expand Down
Loading
Loading