diff --git a/TODO.md b/TODO.md index 966795f..452fb1e 100644 --- a/TODO.md +++ b/TODO.md @@ -170,5 +170,7 @@ have no working implementation in the codebase yet. - [ ] Remove or wire up `fastapi-users` (currently installed but unused) - [x] Replace mock data in stats endpoints with real database queries - [x] Replace mock DNS data with actual DNS lookups -- [ ] Add CI/CD pipeline +- [x] Add CI/CD pipeline — GitHub Actions workflows in `.github/workflows/ci.yml` + (lint → test/security/CodeQL/dependency-review → Docker build/push → GitOps) + and `.github/workflows/release.yml` (semantic versioning) - [ ] Reach >80% test coverage diff --git a/backend/app/templates/report_detail.html b/backend/app/templates/report_detail.html index c623f6a..acc6813 100644 --- a/backend/app/templates/report_detail.html +++ b/backend/app/templates/report_detail.html @@ -51,6 +51,10 @@ Back to Domain + @@ -247,6 +251,27 @@ function reportDetailApp(reportId) { } }, + async deleteReport(domain, reportId) { + if (!confirm(`Delete report "${reportId}" for domain "${domain}"?\n\nThis will remove the report from the system. You can re-import it afterwards.`)) { + return; + } + try { + const response = await fetch( + `/api/v1/reports/domain/${encodeURIComponent(domain)}/reports/${encodeURIComponent(reportId)}`, + { method: 'DELETE' } + ); + if (response.ok) { + window.location.href = '/reports'; + } else { + const data = await response.json().catch(() => ({})); + alert('Failed to delete report: ' + (data.detail || response.statusText)); + } + } catch (error) { + console.error('Error deleting report:', error); + alert('Network error — could not delete report.'); + } + }, + formatDate(timestamp) { if (!timestamp) return '—'; return new Date(timestamp * 1000).toLocaleString(); diff --git a/backend/app/templates/reports.html b/backend/app/templates/reports.html index ed0bb71..087d70d 100644 --- a/backend/app/templates/reports.html +++ b/backend/app/templates/reports.html @@ -95,11 +95,17 @@ {% endcall %} {% call td("text-right") %} - - {% call button(variant="outline", size="sm") %} - View Details - {% endcall %} - +
{% endcall %} {% endcall %} @@ -174,6 +180,32 @@ function reportsApp() { } finally { this.loading = false; } + }, + + async deleteReport(domain, reportId) { + if (!confirm(`Delete report "${reportId}" for domain "${domain}"?\n\nThis will remove the report from the system. You can re-import it afterwards.`)) { + return; + } + try { + const response = await fetch( + `/api/v1/reports/domain/${encodeURIComponent(domain)}/reports/${encodeURIComponent(reportId)}`, + { method: 'DELETE' } + ); + if (response.ok) { + this.reports = this.reports.filter( + r => !(r.domain === domain && r.report_id === reportId) + ); + if (!this.reports.some(r => r.domain === domain)) { + this.domains = this.domains.filter(d => d !== domain); + } + } else { + const data = await response.json().catch(() => ({})); + alert('Failed to delete report: ' + (data.detail || response.statusText)); + } + } catch (error) { + console.error('Error deleting report:', error); + alert('Network error — could not delete report.'); + } } } } diff --git a/docs/development/ci-cd.md b/docs/development/ci-cd.md new file mode 100644 index 0000000..fb94227 --- /dev/null +++ b/docs/development/ci-cd.md @@ -0,0 +1,175 @@ +# CI/CD Pipeline + +DMARQ uses GitHub Actions for all continuous integration and delivery tasks. +The pipeline is defined in two workflow files: + +| File | Purpose | Triggers | +|------|---------|----------| +| `.github/workflows/ci.yml` | Lint → Test → Security → Docker → GitOps | Push to `main`/`develop`, pull requests, weekly schedule | +| `.github/workflows/release.yml` | Semantic versioning & changelog | Push to `main` | + +--- + +## Pipeline Stages + +### Stage 1 — Lint (blocking gate) + +All subsequent jobs depend on this stage succeeding. + +| Tool | What it checks | +|------|---------------| +| **Black** | Code formatting (line length 100, target Python 3.13) | +| **isort** | Import ordering (Black-compatible profile) | +| **Flake8** | Style and complexity (`max-complexity=10`, E203/W503/E501 ignored) | +| **Pylint** | Deeper static analysis (`continue-on-error` — advisory only) | + +The lint job auto-formats with Black and isort before running the `--check` +step, so a failing lint job is usually caused by a Flake8 or Pylint issue. + +### Stage 2 — Parallel quality gates + +These jobs run in parallel once lint passes. + +#### Test + +```bash +cd backend +pytest --cov=app --cov-report=xml --cov-report=term-missing +``` + +Coverage results are uploaded to [Codecov](https://codecov.io). + +#### Security Scan + +- **Bandit** — Python security linter; the JSON report is uploaded as a + workflow artifact (`bandit-security-report`) for every run. +- **pip-audit** — checks all packages in `backend/requirements.txt` against + known CVE databases. + +Both steps use `continue-on-error: true` so they are advisory; a finding will +not block the build but will appear in the workflow summary. + +#### CodeQL Analysis + +GitHub's semantic code analysis scans the Python source for known vulnerability +patterns (security and quality queries). Runs on push and PR events only — +skipped on the weekly scheduled scan. + +#### Dependency Review + +Runs on pull requests only. Fails if any new dependency introduces a +vulnerability of `moderate` severity or higher. + +### Stage 3 — Docker Build & Publish + +Runs only on pushes to `main` after both **Test** and **Security** pass. + +- Builds from `./backend/Dockerfile` +- Pushes to `ghcr.io/