Argus is a web platform designed for SOC analysts who want to centralize and track CVEs detected across their managed infrastructure. The core idea: receive scan reports directly via API, visualize by host, open remediation tickets, and get automatically notified when something critical comes out.
No external cloud dependency, no third-party account, everything runs on your premises.
- Multi-scanner ingestion - Vuls, Trivy, OSV-Scanner, CVE Binary Tool, Grype/Syft, Clair v4, Wazuh, OpenVAS/GVM
- Multi-tenant - CVE isolation per tenant (client), global workspace for admins, fine-grained per-user access control
- Differential scan tracking - a new scan replaces the previous state for that host only: fixed CVEs automatically move to "resolved", no duplicates. The diff is strictly scoped per host, never per tenant, so a targeted rescan of one or a few machines (e.g. via AWX) never touches the others
- Ticket <-> CVE synchronization - resolving a ticket marks the vuln as handled; a vuln that disappears on the next scan automatically closes the related ticket. Matching is keyed on (hostname, tenant), not on the scanner that reported it, so a CVE found by Vuls and absent from a later Trivy scan of the same host closes its ticket just as well - as long as the same
?host=is used across scanners (see API) - Real-time dashboard - active CVEs per host, CVSS scores, KEV flag (CISA), EPSS
- Threat Intelligence - automatic enrichment via CISA KEV (active exploitation), EPSS (30-day exploit probability) and NVD; combined risk score (CVSS x EPSS, KEV bonus) that prioritizes tickets and the dashboard
- Asset criticality - 1->5 rating per host ("crown jewel"), adjustable on the host page - informational for now: not yet reflected in the stored risk score (see Threat Intelligence)
- Remediation SLA - deadlines by severity (Critical 7d, High 30d...), overdue table
- Executive report - printable view / PDF export per tenant (KEV, top-risk, KPIs)
- CVE search - full-text search on CVE IDs, packages, descriptions
- Remediation tickets - creation, assignment, statuses (open -> in progress -> resolved), Markdown comments with images (upload + copy-paste)
- AWX/Ansible remediation - launches an AWX Job Template on a specific host (
--limit) directly from its page or a ticket - Webhooks & notifications - HMAC-SHA256 signed JSON payload, or formatted Slack / Discord / Microsoft Teams messages
- 3 user roles - admin, analyst, viewer (+ per-tenant role)
- Two-factor authentication (2FA TOTP) - Google Authenticator, Aegis, 1Password... per account
- Audit log - all actions tracked with IP and timestamp
- Keycloak SSO - optional, via OIDC
- Security - strict CSP with nonce (no
unsafe-inlinefor scripts), Argon2id, CSRF, rate-limiting - Dark / light mode
- Bilingual interface - French / English, one-click toggle (language icon), saved in session
- Built-in documentation - step-by-step guides in the app (
/docs): scanner configuration, security, webhooks (+ Zulip/ntfy recipes), Keycloak, deployment...
| Component | Technology |
|---|---|
| Backend | Python 3.11 + Flask 3 |
| ORM | SQLAlchemy 2 |
| Database | SQLite (dev) or MariaDB (prod) |
| Authentication | Flask-Login + Argon2id + optional Keycloak OIDC |
| API Security | Bearer keys (SHA-256 in DB), HMAC-SHA256 signatures |
| Frontend | Vanilla HTML/CSS/JS - no SPA framework (2 vendored libraries: Chart.js for graphs, marked.js for Markdown rendering) |
The recommended method. No dependencies to install other than Docker.
git clone https://github.com/your-org/argus.git
cd argus
cp .env.example .env
# Edit .env and set a real SECRET_KEY:
# python -c "import secrets; print(secrets.token_hex(32))"
docker compose up -d --buildOpen http://localhost:5000 and complete the setup wizard to create the first admin account.
Persistence - the SQLite database and images uploaded in tickets are stored in the Docker volume argus_data (mounted on /data). They survive docker compose down / rebuilds. The database is created and migrated automatically at container startup.
Database - SQLite by default. For MariaDB/MySQL, uncomment the db service in docker-compose.yml and set DATABASE_URL in .env (see Configuration).
Update:
git pull
docker compose up -d --build # schema migrations apply automaticallyPython 3.11+ required.
git clone https://github.com/your-org/argus.git
cd argus
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # Edit variables, especially SECRET_KEY
# Development / first launch (creates and migrates the database automatically):
python app.py
# Production (gunicorn - initialize the database first):
python -c "import app; app.init_db()"
gunicorn -w 4 -b 0.0.0.0:5000 --timeout 120 app:appIn production, always run
app.init_db()once before gunicorn: gunicorn workers do not execute the startup block ofapp.py. (The Docker image handles this automatically.)
Images uploaded in tickets are written to static/uploads/ -
remember to back up this folder and the argus.db file.
The fastest way to try Argus on a machine that has only Python 3.11+ (no Docker). A temporary SECRET_KEY is generated on the fly - nothing else to configure.
Linux / macOS
git clone https://github.com/your-org/argus.git && cd argus
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")
python app.pyWindows (PowerShell)
git clone https://github.com/your-org/argus.git; cd argus
python -m venv .venv; .\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
$env:SECRET_KEY = python -c "import secrets; print(secrets.token_hex(32))"
python app.pyThen open http://localhost:5000 and complete the setup wizard to create the first admin account. The SQLite database (argus.db) is created and migrated automatically on first launch.
This mode is reserved for local testing (Flask dev server, ephemeral key). In production, set a persistent
SECRET_KEYand use gunicorn as described above.
To load demo data (6 hosts, 11 real CVEs), create an API key in Administration -> API Keys, then:
python argus_test_data.py --url http://localhost:5000 --key argus_YOUR_KEYEverything is configured through environment variables (or the .env file):
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
required | Flask signing key - generate with python3 -c "import secrets; print(secrets.token_hex(32))" |
DATABASE_URL |
local SQLite | MariaDB format: mysql+pymysql://user:pass@host/db |
HTTPS |
false |
Set to true to enable Secure cookies and HSTS |
FLASK_DEBUG |
false |
Never enable in production |
PORT |
5000 |
Listening port |
KEYCLOAK_ENABLED |
false |
Enables Keycloak OIDC SSO |
KEYCLOAK_SERVER_URL |
- | E.g.: https://keycloak.example.com |
KEYCLOAK_REALM |
argus |
Keycloak realm name |
KEYCLOAK_CLIENT_ID |
argus |
OIDC client ID |
KEYCLOAK_CLIENT_SECRET |
- | OIDC client secret |
Argus is delivered with no real data, accounts, or secrets. Before deploying, check this list:
-
SECRET_KEY- set a real random value (python -c "import secrets; print(secrets.token_hex(32))"). The Docker image default (change_me) and the.env.exampleplaceholder must be replaced. - First admin account - created via the wizard on first launch (
/setup). Choose a strong password; there are no default credentials. -
DATABASE_URL- empty for SQLite (dev), or point to MariaDB/MySQL in production. -
HTTPS=true- enable behind TLS so that session cookies areSecureand HSTS is sent. -
FLASK_DEBUG- leavefalsein production. - Keycloak SSO (optional) - only fill in the
KEYCLOAK_*variables if you use it; otherwise leaveKEYCLOAK_ENABLED=false. - Demo data - the
argus_test_data.pyscript andtests/fixtures/contain fictional clients (Acme Corp, Globex...) and sample hosts. Do not load them on a production instance. -
argus.db- the local database is git-ignored; never commit it. A new database is created on first startup. - Customization (optional) - adjust the app name / login subtitle in
templates/login.htmlandtemplates/base.html.
server {
listen 443 ssl http2;
server_name argus.yourdomain.com;
ssl_certificate /etc/ssl/argus.crt;
ssl_certificate_key /etc/ssl/argus.key;
ssl_protocols TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
}
}All write routes require an API key generated in Administration -> API Keys.
Authorization: Bearer argus_<your-key>
Content-Type: application/json
Rate limit: 200 requests/minute per IP on all /api/v1/import/* and /api/v1/vuls/report endpoints.
GET /api/v1/status
Returns {"status": "ok", "app": "Argus", "version": "1.0.0"} - no authentication required.
Each endpoint accepts the native JSON from the corresponding scanner, plus two optional query string parameters:
| Parameter | Required | Description |
|---|---|---|
?host= |
Sometimes | Hostname. Required for OSV-Scanner, CVE Binary Tool, and OpenVAS when it cannot be detected automatically. |
?client= |
No | Client name to group hosts. Default: Unknown. |
Cross-scanner correlation - the resolved/auto-close diff (see Differential scan tracking) is keyed on the exact pair (hostname, tenant), never on the scanner that produced the report. If the same machine is scanned by several tools (e.g. Vuls, then later Trivy), always pass the same
?host=<name>explicitly on every import. Otherwise, scanners that auto-detect a name from scan metadata (Trivy'sArtifactName, Grype'ssource.target.userInput, Clair'smanifest_hash...) will create a separate host and correlation silently breaks.
Common response for all imports:
{ "ok": true, "host": "srv-01", "cves_processed": 14, "new_cves": 3 }POST /api/v1/vuls/report
curl -X POST "https://argus.yourdomain.com/api/v1/vuls/report" \
-H "Authorization: Bearer argus_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @/var/vuls/results/current/srv-01.jsonPOST /api/v1/import/trivy
# Scan + send in one command
trivy image --format json --quiet ubuntu:22.04 | \
curl -X POST "https://argus.yourdomain.com/api/v1/import/trivy?client=Prod" \
-H "Authorization: Bearer argus_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @-The hostname is extracted from ArtifactName. Override with ?host=.
?host=required - OSV-Scanner does not include the machine name in its report.
POST /api/v1/import/osv
osv-scanner --format json --output report.json /path/to/project
curl -X POST "https://argus.yourdomain.com/api/v1/import/osv?host=my-server&client=Prod" \
-H "Authorization: Bearer argus_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @report.json
?host=required - same reason as OSV-Scanner.
POST /api/v1/import/cve-binary-tool
cve-bin-tool --format json --output report.json /usr/bin/
curl -X POST "https://argus.yourdomain.com/api/v1/import/cve-binary-tool?host=my-server&client=Prod" \
-H "Authorization: Bearer argus_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @report.jsonPOST /api/v1/import/grype
# Direct scan
grype ubuntu:22.04 -o json | \
curl -X POST "https://argus.yourdomain.com/api/v1/import/grype?client=Prod" \
-H "Authorization: Bearer argus_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @-
# Via Syft SBOM
syft ubuntu:22.04 -o json > sbom.json
grype sbom:sbom.json -o json | \
curl -X POST "https://argus.yourdomain.com/api/v1/import/grype?host=ubuntu-22.04&client=Prod" \
-H "Authorization: Bearer argus_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @-POST /api/v1/import/clair
MANIFEST="sha256:abc123..."
curl -s "http://clair:6060/matcher/api/v1/vulnerability_report/${MANIFEST}" | \
curl -X POST "https://argus.yourdomain.com/api/v1/import/clair?host=my-image&client=Prod" \
-H "Authorization: Bearer argus_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @-Clair v4 does not provide numeric CVSS scores - the text severity (Critical/High/Medium/Low/Negligible) is used directly.
Two accepted formats:
Format 1 - Wazuh REST API (GET /vulnerability/{agent_id}):
POST /api/v1/import/wazuh
TOKEN=$(curl -su admin:admin -X POST "https://wazuh-manager:55000/security/user/authenticate" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")
curl -sk -H "Authorization: Bearer $TOKEN" \
"https://wazuh-manager:55000/vulnerability/001?limit=500" | \
curl -X POST "https://argus.yourdomain.com/api/v1/import/wazuh?client=Prod" \
-H "Authorization: Bearer argus_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @-Format 2 - Alerts array (export from Wazuh Indexer/OpenSearch):
curl -X POST "https://argus.yourdomain.com/api/v1/import/wazuh?host=agent-name&client=Prod" \
-H "Authorization: Bearer argus_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @wazuh-alerts.jsonOpenVAS exports in XML. Convert first with xmltodict:
POST /api/v1/import/openvas
pip install python-gvm xmltodict
REPORT_ID="your-report-uuid"
gvm-cli --gmp-username admin --gmp-password admin socket \
--xml "<get_reports report_id='${REPORT_ID}' filter='rows=-1'/>" | \
python3 -c "import sys,json,xmltodict; print(json.dumps(xmltodict.parse(sys.stdin.read())))" | \
curl -X POST "https://argus.yourdomain.com/api/v1/import/openvas?host=target&client=Prod" \
-H "Authorization: Bearer argus_YOUR_KEY" \
-H "Content-Type: application/json" \
-d @-CVEs are extracted from the nvt.refs.ref field (type="cve"). Results without a valid CVE are ignored.
Configure outbound destinations in Administration -> Webhooks. Choose the destination type:
| Type | Format sent |
|---|---|
generic |
Full HMAC-SHA256 signed JSON payload (X-Argus-Signature header) |
slack |
Formatted message for a Slack Incoming Webhook ({"text": "..."}) |
discord |
Formatted message for a Discord Webhook |
teams |
MessageCard for a Microsoft Teams Connector |
Three available events:
| Event | Trigger |
|---|---|
new_cve |
New CVE detected on a host |
critical_cve |
New CRITICAL CVE detected |
new_ticket |
New remediation ticket created |
Slack/Discord/Teams receive a readable message (severity, host, CVSS, KEV flag). The
generictype receives the raw signed payload below.
Zulip / ntfy / other tools - no native type, but Argus exposes the same generic endpoint (Slack-compatible for Zulip, or raw JSON for ntfy). The built-in documentation (/docs?section=zulip, /docs?section=ntfy) provides the URL to use and a Python relay script example to reformat the payload if needed.
Payload:
{
"event": "critical_cve",
"timestamp": "2026-06-18T10:00:00+00:00",
"argus_version": "1.0.0",
"host": { "name": "srv-01", "client": "Acme", "ip": "10.0.0.1" },
"cve": {
"id": "CVE-2024-6387",
"severity": "HIGH",
"cvss": 8.1,
"package": "openssh-server",
"fixed_in": "9.7p1"
},
"source": "trivy"
}Signature verification on the receiver side:
import hmac, hashlib
def verify_signature(payload: bytes, secret: str, header: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)The signature is sent in the X-Argus-Signature header.
| Protection | Implementation |
|---|---|
| Password hashing | Argon2id (64 MB memory, 3 iterations, 4 threads) |
| Session cookies | HttpOnly, SameSite=Strict, Secure (if HTTPS=true) |
| CSRF protection | Flask-WTF, token in all forms |
| Rate limiting | 10 login attempts per minute per IP |
| Account lockout | 5 failures -> 15-minute lockout |
| SQL injection | SQLAlchemy ORM only (parameterized queries) |
| XSS | bleach on server fields (titles, descriptions, names); Markdown comments sanitized client-side after rendering (DOMParser, removal of active tags/attributes) |
| API keys | SHA-256 storage only - the raw key is displayed once |
| Webhook signatures | HMAC-SHA256 |
| Two-factor authentication | TOTP (RFC 6238) per account, verified at login |
| Security headers | CSP, X-Frame-Options, X-Content-Type-Options, HSTS |
| Webhook URLs | Schema validation - only http:// and https:// |
| Role | Access |
|---|---|
admin |
Full access: users, tenants, webhooks, API keys, AWX, Threat Intel, audit log |
analyst |
Dashboard, hosts, CVEs, tickets (create, edit, comment) |
viewer |
Read-only: dashboard, hosts, CVEs |
A role can also be assigned per tenant (see below).
Each host belongs to a tenant (a client / organization). CVEs, hosts, and tickets never mix between tenants.
- Workspace selector in the sidebar: switch tenants at any time, the choice is preserved from page to page (stored in session).
- Admins: have a Global (all tenants) view in addition to each tenant individually.
- Non-admins: only see the tenants they are assigned to.
- Access assignment:
Admin -> Tenants-> tenant card -> Add member (user + role). - Creation: a tenant is created automatically for each new
?client=received on import, or manually inAdmin -> Tenants. - Import routing: associate an API key with a tenant (
Admin -> API Keys) to route its reports there; otherwise the tenant is inferred from the?client=parameter.
Argus can trigger an AWX / Ansible Tower Job Template on a specific host.
Admin -> AWX-> add a connection (AWX base URL + Bearer OAuth2 token).- Declare one or more Job Templates (name + template ID in AWX).
- A Launch playbook button then appears on each host page and in each ticket.
On launch, Argus calls POST {awx_url}/api/v2/job_templates/{id}/launch/ with {"limit": "<hostname>"} - the playbook therefore only runs on the relevant machine. The AWX job number is displayed and logged in the audit.
As long as no Job Template is configured, the host page displays a reminder pointing to
Admin -> AWX.
Partial rescans are safe - the resolved/auto-close diff is strictly scoped to the host being re-imported (see Differential scan tracking), so rescanning one or a few machines via AWX never closes tickets on hosts outside that batch, even if the tenant has hundreds of others. If your playbook feeds the rescan result back into Argus, call the same import endpoint with
?host=<hostname>per machine, and re-run the same scanner with the same coverage as the original scan - not a narrow single-CVE check, which would make Argus wrongly resolve every other CVE on that host that the check didn't look at.
Ticket comments and descriptions support Markdown (bold, italic, lists, code, tables, links, quotes).
- Toolbar + Preview button in the comment form.
- Images: camera button to upload, or paste a screenshot directly (Ctrl+V) - the image is uploaded and inserted as
. - Accepted formats: PNG, JPG/JPEG, GIF, WebP (10 MB max). Storage in
static/uploads/. - Markdown is converted to HTML client-side (
marked.js), then sanitized before display by a dedicated DOM sanitizer (removal of active tags and attributes -script,on*,javascript:...). Other fields (titles, descriptions, names) are sanitized server-side withbleach.
Argus enriches each CVE with official public sources, free and without an API key:
| Source | Contribution |
|---|---|
| CISA KEV | Marks CVEs actively exploited in the wild |
| FIRST EPSS | 30-day exploitation probability (0->100%) |
| NVD 2.0 | Missing description + CVSS score (optional, slower) |
Enrichment is automatic after each import (as a background task) and can be relaunched manually in Admin -> Threat Intel.
Risk score (0->100) that prioritizes beyond CVSS alone:
risk = 100 x (0.6 x CVSS/10 + 0.4 x EPSS)
x KEV bonus (x1.3 +10 if exploited)
It feeds the dashboard ranking, host page, and executive report.
Set the asset criticality (1 = low -> 5 = crown jewel) on the host page.
Implementation note -
intel.compute_risk()accepts a criticality weighting factor (0.8 to 1.2 based on 1->5 rating), but no caller currently passes it: therisk_scoreis stored per CVE (not per host+CVE pair), so it is the same regardless of which host reports it. Asset criticality remains for now an informational field displayed on the host page.
Each ticket receives a deadline calculated from the CVE severity:
| Severity | Deadline |
|---|---|
| Critical | 7 days |
| High | 30 days |
| Medium | 90 days |
| Low | 180 days |
The SLA view lists open tickets sorted by deadline and highlights overdue tickets and imminent deadlines (< 3 days).
Report (side menu) generates a summary view of the current scope (active tenant or global): KPIs, priority KEV vulnerabilities, top-risk, open tickets. The Print / Export to PDF button opens the browser's print dialog ("Save as PDF").
Each user activates 2FA from the footer -> 2FA: scan the QR code with Google Authenticator / Aegis / 1Password, confirm with a 6-digit code. A code is then required at each login. Deactivation requires the account password.
An injection script is provided to quickly populate a dev instance:
# First create an API key in Administration -> API Keys
python argus_test_data.py --url http://localhost:5000 --key argus_YOUR_KEYInjects 6 servers (CentOS, Rocky, Windows Server, Ubuntu) with 11 real CVEs including regreSSHion (CVE-2024-6387), XZ backdoor (CVE-2024-3094), Looney Tunables (CVE-2023-4911).
By default, each host is routed to its own tenant (the Client field in the fixture): Acme Corp (3 hosts), Globex, Initech, Umbrella - 4 tenants created automatically. Two additional options:
# Force all hosts sent to a single tenant, ignoring the fixture Client field
python argus_test_data.py --url http://localhost:5000 --key argus_YOUR_KEY --client "My Client"
# Only inject a specific host (combinable with --client)
python argus_test_data.py --url http://localhost:5000 --key argus_YOUR_KEY --host srv-mail-01If the API key used is linked to a specific tenant (
Admin -> API Keys), that tenant takes precedence over--clientand over each host'sClientfield: everything ends up in the key's tenant. Use a key without an assigned tenant ("Auto (from the ?client= parameter)") to benefit from automatic distribution across 4 tenants.
The automated suite (unit + integration) uses pytest on an isolated temporary SQLite database (the real argus.db is never touched):
pip install -r requirements-dev.txt
pytest # full suite
pytest tests/unit # parsers, utils, scoring, helpers
pytest tests/integration # auth, 2FA, imports, tenant isolation, tickets...
pytest --cov=app --cov=intel --cov=parsers --cov-report=term-missingOrganization:
| Folder | Content |
|---|---|
tests/unit/ |
Unit tests (parsers, parsers/utils, intel, app helpers) |
tests/integration/ |
End-to-end via Flask test client |
tests/fixtures/ |
Scanner JSON reports |
tests/manual/ |
Scripts for sending to a real server (not collected by pytest - see their README) |
Argus est une plateforme web conçue pour les analystes SOC qui veulent centraliser et suivre les CVE détectées sur leur parc infogéré. L'idée de base : recevoir les rapports de scan directement via API, visualiser par hôte, ouvrir des tickets de remédiation et être notifié automatiquement quand quelque chose de critique sort.
Pas de dépendance à un cloud externe, pas de compte tiers, tout tourne chez vous.
- Ingestion multi-scanners - Vuls, Trivy, OSV-Scanner, CVE Binary Tool, Grype/Syft, Clair v4, Wazuh, OpenVAS/GVM
- Multi-tenant - isolation des CVE par tenant (client), workspace global pour les admins, gestion fine des accès par utilisateur
- Suivi différentiel des scans - un nouveau scan remplace l'état précédent de cet hôte uniquement : les CVE corrigées passent automatiquement en « résolu », pas de doublons. Le diff est strictement scopé par hôte, jamais par tenant : un rescan ciblé d'une ou plusieurs machines (via AWX par exemple) ne touche jamais les autres
- Synchronisation tickets ↔ CVE - résoudre un ticket marque la vuln traitée ; une vuln qui disparaît au scan suivant ferme automatiquement le ticket lié. La correspondance se fait sur le couple (nom d'hôte, tenant), pas sur l'outil qui l'a remontée : une CVE vue par Vuls et absente d'un scan Trivy ultérieur sur le même hôte ferme aussi bien son ticket - à condition d'utiliser le même
?host=entre scanners (voir API) - Dashboard temps réel - CVE actives par hôte, scores CVSS, flag KEV (CISA), EPSS
- Threat Intelligence - enrichissement automatique via CISA KEV (exploitation active), EPSS (probabilité d'exploit à 30 j) et NVD ; score de risque combiné (CVSS × EPSS, bonus KEV) qui priorise les tickets et le dashboard
- Criticité d'actif - note 1→5 par hôte (« joyau de la couronne »), réglable sur la fiche hôte - informatif pour l'instant : pas encore répercutée dans le score de risque stocké (voir Threat Intelligence)
- SLA de remédiation - échéances par sévérité (Critique 7 j, Haute 30 j…), tableau des dépassements
- Rapport exécutif - vue imprimable / export PDF par tenant (KEV, top-risque, KPIs)
- Recherche CVE - recherche plein texte sur les CVE IDs, packages, descriptions
- Tickets de remédiation - création, assignation, statuts (ouvert → en cours → résolu), commentaires Markdown avec images (upload + copier-coller)
- Remédiation AWX/Ansible - lance un Job Template AWX sur un hôte précis (
--limit) directement depuis sa fiche ou un ticket - Webhooks & notifications - payload JSON signé HMAC-SHA256, ou messages formatés Slack / Discord / Microsoft Teams
- 3 rôles utilisateur - admin, analyste, lecteur (+ rôle par tenant)
- Double authentification (2FA TOTP) - Google Authenticator, Aegis, 1Password… par compte
- Journal d'audit - toutes les actions tracées avec IP et timestamp
- SSO Keycloak - optionnel, via OIDC
- Sécurité - CSP stricte avec nonce (aucun
unsafe-inlinepour les scripts), Argon2id, CSRF, rate-limiting - Mode sombre / clair
- Interface bilingue - français / anglais, bascule en un clic (icône langue), conservée en session
- Documentation intégrée - guides pas-à-pas dans l'app (
/docs) : configuration par scanner, sécurité, webhooks (+ recettes Zulip/ntfy), Keycloak, déploiement…
| Composant | Technologie |
|---|---|
| Backend | Python 3.11 + Flask 3 |
| ORM | SQLAlchemy 2 |
| Base de données | SQLite (dev) ou MariaDB (prod) |
| Authentification | Flask-Login + Argon2id + Keycloak OIDC optionnel |
| Sécurité API | Clés Bearer (SHA-256 en base), signatures HMAC-SHA256 |
| Frontend | HTML/CSS/JS vanilla - aucun framework SPA (2 librairies vendorisées : Chart.js pour les graphiques, marked.js pour le rendu Markdown) |
La méthode recommandée. Aucune dépendance à installer hormis Docker.
git clone https://github.com/your-org/argus.git
cd argus
cp .env.example .env
# Éditez .env et mettez une vraie SECRET_KEY :
# python -c "import secrets; print(secrets.token_hex(32))"
docker compose up -d --buildOuvrez http://localhost:5000 et terminez l'assistant de configuration pour créer le premier compte admin.
Persistance - la base SQLite et les images uploadées dans les tickets sont stockées dans le volume Docker argus_data (monté sur /data). Elles survivent aux docker compose down / rebuilds. La base est créée et migrée automatiquement au démarrage du conteneur.
Base de données - SQLite par défaut. Pour MariaDB/MySQL, décommentez le service db dans docker-compose.yml et renseignez DATABASE_URL dans .env (voir Configuration).
Mise à jour :
git pull
docker compose up -d --build # les migrations de schéma s'appliquent seulesPython 3.11+ requis.
git clone https://github.com/your-org/argus.git
cd argus
python3 -m venv .venv
source .venv/bin/activate # Windows : .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # Éditez les variables, surtout SECRET_KEY
# Développement / premier lancement (crée et migre la base automatiquement) :
python app.py
# Production (gunicorn - initialisez d'abord la base) :
python -c "import app; app.init_db()"
gunicorn -w 4 -b 0.0.0.0:5000 --timeout 120 app:appEn production, lancez toujours
app.init_db()une fois avant gunicorn : les workers gunicorn n'exécutent pas le bloc de démarrage deapp.py. (L'image Docker s'en charge automatiquement.)
Les images uploadées dans les tickets sont écrites dans static/uploads/ -
pensez à sauvegarder ce dossier et le fichier argus.db.
La façon la plus rapide d'essayer Argus sur une machine qui n'a que Python 3.11+ (pas de Docker). Une SECRET_KEY temporaire est générée à la volée - rien d'autre à configurer.
Linux / macOS
git clone https://github.com/your-org/argus.git && cd argus
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")
python app.pyWindows (PowerShell)
git clone https://github.com/your-org/argus.git; cd argus
python -m venv .venv; .\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
$env:SECRET_KEY = python -c "import secrets; print(secrets.token_hex(32))"
python app.pyOuvrez ensuite http://localhost:5000 et complétez l'assistant d'installation pour créer le premier compte admin. La base SQLite (argus.db) est créée et migrée automatiquement au premier lancement.
Ce mode est réservé au test local (serveur de dev Flask, clé éphémère). En production, fixez une
SECRET_KEYpersistante et utilisez gunicorn comme indiqué plus haut.
Pour charger des données de démo (6 hôtes, 11 CVE réelles), créez une clé API dans Administration → Clés API, puis :
python argus_test_data.py --url http://localhost:5000 --key argus_VOTRE_CLETout passe par les variables d'environnement (ou le fichier .env) :
| Variable | Par défaut | Description |
|---|---|---|
SECRET_KEY |
obligatoire | Clé de signature Flask - générez avec python3 -c "import secrets; print(secrets.token_hex(32))" |
DATABASE_URL |
SQLite local | Format MariaDB : mysql+pymysql://user:pass@host/db |
HTTPS |
false |
Passer à true pour activer les cookies Secure et HSTS |
FLASK_DEBUG |
false |
Ne jamais activer en production |
PORT |
5000 |
Port d'écoute |
KEYCLOAK_ENABLED |
false |
Active le SSO Keycloak OIDC |
KEYCLOAK_SERVER_URL |
- | Ex : https://keycloak.example.com |
KEYCLOAK_REALM |
argus |
Nom du realm Keycloak |
KEYCLOAK_CLIENT_ID |
argus |
ID client OIDC |
KEYCLOAK_CLIENT_SECRET |
- | Secret client OIDC |
Argus est livré sans aucune donnée réelle, compte, ni secret. Avant de déployer, vérifiez cette liste :
-
SECRET_KEY- mettez une vraie valeur aléatoire (python -c "import secrets; print(secrets.token_hex(32))"). Le défaut de l'image Docker (changez_moi) et le placeholder de.env.exampledoivent être remplacés. - Premier compte admin - créé via l'assistant au premier lancement (
/setup). Choisissez un mot de passe fort ; il n'y a aucun identifiant par défaut. -
DATABASE_URL- vide pour SQLite (dev), ou pointez vers MariaDB/MySQL en production. -
HTTPS=true- à activer derrière du TLS pour que les cookies de session soientSecureet que HSTS soit envoyé. -
FLASK_DEBUG- laissezfalseen production. - SSO Keycloak (optionnel) - ne renseignez les
KEYCLOAK_*que si vous l'utilisez ; sinon laissezKEYCLOAK_ENABLED=false. - Données de démo - le script
argus_test_data.pyettests/fixtures/contiennent des clients fictifs (Acme Corp, Globex…) et des hôtes d'exemple. Ne les chargez pas sur une instance de production. -
argus.db- la base locale est git-ignorée ; ne la committez jamais. Une base neuve est créée au premier démarrage. - Personnalisation (optionnel) - ajustez le nom de l'app / le sous-titre de connexion dans
templates/login.htmlettemplates/base.html.
server {
listen 443 ssl http2;
server_name argus.mondomaine.fr;
ssl_certificate /etc/ssl/argus.crt;
ssl_certificate_key /etc/ssl/argus.key;
ssl_protocols TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
}
}Toutes les routes d'écriture nécessitent une clé API générée dans Administration → Clés API.
Authorization: Bearer argus_<votre-cle>
Content-Type: application/json
Limite de débit : 200 requêtes/minute par IP sur tous les endpoints /api/v1/import/* et /api/v1/vuls/report.
GET /api/v1/status
Retourne {"status": "ok", "app": "Argus", "version": "1.0.0"} - sans authentification.
Chaque endpoint accepte le JSON natif du scanner correspondant, plus deux paramètres optionnels en query string :
| Paramètre | Requis | Description |
|---|---|---|
?host= |
Parfois | Nom d'hôte. Obligatoire pour OSV-Scanner, CVE Binary Tool et OpenVAS quand il ne peut pas être détecté automatiquement. |
?client= |
Non | Nom du client pour regrouper les hôtes. Défaut : Inconnu. |
Corrélation entre scanners - le diff résolu/fermeture-auto (voir Suivi différentiel des scans) se base sur le couple exact (nom d'hôte, tenant), jamais sur le scanner qui a produit le rapport. Si une même machine est scannée par plusieurs outils (ex : Vuls, puis plus tard Trivy), passez toujours le même
?host=<nom>explicitement sur chaque import. Sinon, les scanners qui déduisent un nom depuis les métadonnées du scan (ArtifactNamepour Trivy,source.target.userInputpour Grype,manifest_hashpour Clair…) créeront un hôte distinct et la corrélation se cassera silencieusement.
Réponse commune à tous les imports :
{ "ok": true, "host": "srv-01", "cves_processed": 14, "new_cves": 3 }POST /api/v1/vuls/report
curl -X POST "https://argus.mondomaine.fr/api/v1/vuls/report" \
-H "Authorization: Bearer argus_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d @/var/vuls/results/current/srv-01.jsonPOST /api/v1/import/trivy
# Scan + envoi en une commande
trivy image --format json --quiet ubuntu:22.04 | \
curl -X POST "https://argus.mondomaine.fr/api/v1/import/trivy?client=Prod" \
-H "Authorization: Bearer argus_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d @-Le nom d'hôte est extrait de ArtifactName. Remplacez-le avec ?host=.
?host=obligatoire - OSV-Scanner n'inclut pas le nom de la machine dans son rapport.
POST /api/v1/import/osv
osv-scanner --format json --output rapport.json /chemin/vers/projet
curl -X POST "https://argus.mondomaine.fr/api/v1/import/osv?host=mon-serveur&client=Prod" \
-H "Authorization: Bearer argus_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d @rapport.json
?host=obligatoire - même raison qu'OSV-Scanner.
POST /api/v1/import/cve-binary-tool
cve-bin-tool --format json --output rapport.json /usr/bin/
curl -X POST "https://argus.mondomaine.fr/api/v1/import/cve-binary-tool?host=mon-serveur&client=Prod" \
-H "Authorization: Bearer argus_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d @rapport.jsonPOST /api/v1/import/grype
# Scan direct
grype ubuntu:22.04 -o json | \
curl -X POST "https://argus.mondomaine.fr/api/v1/import/grype?client=Prod" \
-H "Authorization: Bearer argus_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d @-
# Via SBOM Syft
syft ubuntu:22.04 -o json > sbom.json
grype sbom:sbom.json -o json | \
curl -X POST "https://argus.mondomaine.fr/api/v1/import/grype?host=ubuntu-22.04&client=Prod" \
-H "Authorization: Bearer argus_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d @-POST /api/v1/import/clair
MANIFEST="sha256:abc123..."
curl -s "http://clair:6060/matcher/api/v1/vulnerability_report/${MANIFEST}" | \
curl -X POST "https://argus.mondomaine.fr/api/v1/import/clair?host=mon-image&client=Prod" \
-H "Authorization: Bearer argus_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d @-Clair v4 ne fournit pas de scores CVSS numériques - la sévérité textuelle (Critical/High/Medium/Low/Negligible) est utilisée directement.
Deux formats acceptés :
Format 1 - API REST Wazuh (GET /vulnerability/{agent_id}) :
POST /api/v1/import/wazuh
TOKEN=$(curl -su admin:admin -X POST "https://wazuh-manager:55000/security/user/authenticate" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")
curl -sk -H "Authorization: Bearer $TOKEN" \
"https://wazuh-manager:55000/vulnerability/001?limit=500" | \
curl -X POST "https://argus.mondomaine.fr/api/v1/import/wazuh?client=Prod" \
-H "Authorization: Bearer argus_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d @-Format 2 - Tableau d'alertes (export depuis Wazuh Indexer/OpenSearch) :
curl -X POST "https://argus.mondomaine.fr/api/v1/import/wazuh?host=nom-agent&client=Prod" \
-H "Authorization: Bearer argus_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d @alertes-wazuh.jsonOpenVAS exporte en XML. Convertissez d'abord avec xmltodict :
POST /api/v1/import/openvas
pip install python-gvm xmltodict
REPORT_ID="votre-uuid-rapport"
gvm-cli --gmp-username admin --gmp-password admin socket \
--xml "<get_reports report_id='${REPORT_ID}' filter='rows=-1'/>" | \
python3 -c "import sys,json,xmltodict; print(json.dumps(xmltodict.parse(sys.stdin.read())))" | \
curl -X POST "https://argus.mondomaine.fr/api/v1/import/openvas?host=cible&client=Prod" \
-H "Authorization: Bearer argus_VOTRE_CLE" \
-H "Content-Type: application/json" \
-d @-Les CVE sont extraites du champ nvt.refs.ref (type="cve"). Les résultats sans CVE valide sont ignorés.
Configurez les destinations sortantes dans Administration → Webhooks. Choisissez le type de destination :
| Type | Format envoyé |
|---|---|
generic |
Payload JSON complet signé HMAC-SHA256 (en-tête X-Argus-Signature) |
slack |
Message formaté pour un Incoming Webhook Slack ({"text": "..."}) |
discord |
Message formaté pour un Webhook Discord |
teams |
Carte MessageCard pour un Connector Microsoft Teams |
Trois événements disponibles :
| Événement | Déclencheur |
|---|---|
new_cve |
Nouvelle CVE détectée sur un hôte |
critical_cve |
Nouvelle CVE CRITICAL détectée |
new_ticket |
Nouveau ticket de remédiation créé |
Slack/Discord/Teams reçoivent un message lisible (sévérité, hôte, CVSS, flag KEV). Le type
genericreçoit le payload brut signé ci-dessous.
Zulip / ntfy / autre outil - pas de type natif, mais Argus expose le même endpoint generic (compatible Slack pour Zulip, ou JSON brut pour ntfy). La documentation intégrée (/docs?section=zulip, /docs?section=ntfy) fournit l'URL à utiliser et un exemple de script relais Python pour reformater le payload si besoin.
Payload :
{
"event": "critical_cve",
"timestamp": "2026-06-18T10:00:00+00:00",
"argus_version": "1.0.0",
"host": { "name": "srv-01", "client": "Acme", "ip": "10.0.0.1" },
"cve": {
"id": "CVE-2024-6387",
"severity": "HIGH",
"cvss": 8.1,
"package": "openssh-server",
"fixed_in": "9.7p1"
},
"source": "trivy"
}Vérification de signature côté récepteur :
import hmac, hashlib
def verifier_signature(payload: bytes, secret: str, header: str) -> bool:
attendu = "sha256=" + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(attendu, header)La signature est envoyée dans l'en-tête X-Argus-Signature.
| Protection | Implémentation |
|---|---|
| Hachage des mots de passe | Argon2id (64 Mo mémoire, 3 itérations, 4 threads) |
| Cookies de session | HttpOnly, SameSite=Strict, Secure (si HTTPS=true) |
| Protection CSRF | Flask-WTF, jeton dans tous les formulaires |
| Rate limiting | 10 tentatives de connexion par minute par IP |
| Verrouillage de compte | 5 échecs → verrouillage 15 minutes |
| Injection SQL | SQLAlchemy ORM uniquement (requêtes paramétrées) |
| XSS | bleach sur les champs serveur (titres, descriptions, noms) ; commentaires Markdown sanitisés côté client après rendu (DOMParser, suppression des balises/attributs actifs) |
| Clés API | Stockage SHA-256 uniquement - la clé brute s'affiche une seule fois |
| Signatures webhook | HMAC-SHA256 |
| Double authentification | TOTP (RFC 6238) par compte, vérification à la connexion |
| En-têtes de sécurité | CSP, X-Frame-Options, X-Content-Type-Options, HSTS |
| URLs webhook | Validation du schéma - uniquement http:// et https:// |
| Rôle | Accès |
|---|---|
admin |
Accès complet : utilisateurs, tenants, webhooks, clés API, AWX, Threat Intel, journal d'audit |
analyst |
Dashboard, hôtes, CVE, tickets (création, modification, commentaires) |
viewer |
Lecture seule : dashboard, hôtes, CVE |
Un rôle peut aussi être attribué par tenant (voir ci-dessous).
Chaque hôte appartient à un tenant (un client / une organisation). Les CVE, hôtes et tickets ne se mélangent jamais entre tenants.
- Sélecteur de workspace dans la barre latérale : changez de tenant à tout moment, le choix est conservé d'une page à l'autre (stocké en session).
- Admins : disposent d'une vue Global (tous les tenants) en plus de chaque tenant pris isolément.
- Non-admins : ne voient que les tenants auxquels ils sont rattachés.
- Attribution des accès :
Admin → Tenants→ carte du tenant → Ajouter un membre (utilisateur + rôle). - Création : un tenant est créé automatiquement pour chaque nouveau
?client=reçu à l'import, ou manuellement dansAdmin → Tenants. - Routage des imports : associez une clé API à un tenant (
Admin → Clés API) pour y ranger ses rapports ; sinon le tenant est déduit du paramètre?client=.
Argus peut déclencher un Job Template AWX / Ansible Tower sur un hôte précis.
Admin → AWX→ ajoutez une connexion (URL de base AWX + token Bearer OAuth2).- Déclarez un ou plusieurs Job Templates (nom + ID du template dans AWX).
- Un bouton Lancer le playbook apparaît alors sur chaque fiche d'hôte et dans chaque ticket.
Au lancement, Argus appelle POST {awx_url}/api/v2/job_templates/{id}/launch/ avec {"limit": "<hostname>"} - le playbook ne s'exécute donc que sur la machine concernée. Le numéro du job AWX est affiché et journalisé dans l'audit.
Tant qu'aucun Job Template n'est configuré, la fiche d'hôte affiche un rappel pointant vers
Admin → AWX.
Les rescans partiels sont sans risque - le diff résolu/fermeture-auto est strictement scopé à l'hôte en cours d'import (voir Suivi différentiel des scans), donc rescanner une ou quelques machines via AWX ne ferme jamais les tickets des hôtes absents de ce lot, même si le tenant en compte des centaines d'autres. Si votre playbook réinjecte le résultat du rescan dans Argus, appelez le même endpoint d'import avec
?host=<nom>par machine, et relancez le même scanner avec la même couverture que le scan initial - pas une simple vérification ponctuelle d'une CVE, qui ferait résoudre à tort toutes les autres CVE de cet hôte que le contrôle n'a pas examinées.
Les commentaires de tickets et leur description supportent le Markdown (gras, italique, listes, code, tableaux, liens, citations).
- Barre d'outils + bouton Aperçu dans le formulaire de commentaire.
- Images : bouton pour téléverser, ou collez directement une capture (Ctrl+V) - l'image est uploadée et insérée en
. - Formats acceptés : PNG, JPG/JPEG, GIF, WebP (10 Mo max). Stockage dans
static/uploads/. - Le Markdown est converti en HTML côté client (
marked.js), puis assaini avant affichage par un sanitiseur DOM dédié (suppression des balises et attributs actifs -script,on*,javascript:…). Les autres champs (titres, descriptions, noms) sont assainis côté serveur avecbleach.
Argus enrichit chaque CVE avec des sources publiques officielles, gratuites et sans clé API :
| Source | Apport |
|---|---|
| CISA KEV | Marque les CVE activement exploitées dans la nature |
| FIRST EPSS | Probabilité d'exploitation à 30 jours (0→100 %) |
| NVD 2.0 | Description + score CVSS manquants (optionnel, plus lent) |
L'enrichissement est automatique après chaque import (en tâche de fond) et peut être relancé manuellement dans Admin → Threat Intel.
Score de risque (0→100) qui priorise au-delà du seul CVSS :
risque = 100 × (0.6 × CVSS/10 + 0.4 × EPSS)
× bonus KEV (×1.3 +10 si exploitée)
Il alimente le classement du dashboard, la fiche d'hôte et le rapport exécutif.
Réglez la criticité d'un actif (1 = faible → 5 = joyau de la couronne) sur sa fiche d'hôte.
Note d'implémentation -
intel.compute_risk()accepte un facteur de pondération par criticité (0.8 à 1.2 selon la note 1→5), mais aucun appelant ne le transmet actuellement : lerisk_scoreest stocké par CVE (pas par couple hôte+CVE), donc identique quel que soit l'hôte qui la remonte. La criticité d'actif reste pour l'instant un champ informatif affiché sur la fiche hôte.
Chaque ticket reçoit une échéance calculée depuis la sévérité de la CVE :
| Sévérité | Délai |
|---|---|
| Critique | 7 jours |
| Haute | 30 jours |
| Moyenne | 90 jours |
| Basse | 180 jours |
La vue SLA liste les tickets ouverts triés par échéance et signale les dépassements et échéances imminentes (< 3 j).
Rapport (menu latéral) génère une vue de synthèse du périmètre courant (tenant actif ou global) : KPIs, vulnérabilités KEV prioritaires, top-risque, tickets ouverts. Le bouton Imprimer / Exporter en PDF ouvre la boîte d'impression du navigateur (« Enregistrer au format PDF »).
Chaque utilisateur active la 2FA depuis le footer → 2FA : scannez le QR code avec Google Authenticator / Aegis / 1Password, confirmez avec un code à 6 chiffres. Un code est ensuite demandé à chaque connexion. La désactivation exige le mot de passe du compte.
Un script d'injection est fourni pour peupler rapidement une instance de dev :
# Créez d'abord une clé API dans Administration → Clés API
python argus_test_data.py --url http://localhost:5000 --key argus_VOTRE_CLEInjecte 6 serveurs (CentOS, Rocky, Windows Server, Ubuntu) avec 11 CVE réelles dont regreSSHion (CVE-2024-6387), XZ backdoor (CVE-2024-3094), Looney Tunables (CVE-2023-4911).
Par défaut, chaque hôte est routé vers son propre tenant (champ Client du fixture) : Acme Corp (3 hôtes), Globex, Initech, Umbrella - 4 tenants créés automatiquement. Deux options supplémentaires :
# Forcer tous les hôtes envoyés vers un seul tenant, en ignorant le champ Client du fixture
python argus_test_data.py --url http://localhost:5000 --key argus_VOTRE_CLE --client "Mon Client"
# N'injecter qu'un hôte précis (combinable avec --client)
python argus_test_data.py --url http://localhost:5000 --key argus_VOTRE_CLE --host srv-mail-01Si la clé API utilisée est rattachée à un tenant spécifique (
Admin → Clés API), ce tenant prime sur--clientet sur leClientde chaque hôte : tout finit dans le tenant de la clé. Utilisez une clé sans tenant assigné (« Auto (depuis le paramètre ?client=) ») pour profiter de la répartition automatique sur 4 tenants.
La suite automatisée (unitaire + intégration) utilise pytest sur une base SQLite temporaire isolée (la vraie argus.db n'est jamais touchée) :
pip install -r requirements-dev.txt
pytest # toute la suite
pytest tests/unit # parsers, utils, scoring, helpers
pytest tests/integration # auth, 2FA, imports, isolation tenant, tickets…
pytest --cov=app --cov=intel --cov=parsers --cov-report=term-missingOrganisation :
| Dossier | Contenu |
|---|---|
tests/unit/ |
Tests unitaires (parsers, parsers/utils, intel, helpers d'app) |
tests/integration/ |
Bout-en-bout via le client de test Flask |
tests/fixtures/ |
Rapports JSON des scanners |
tests/manual/ |
Scripts d'envoi vers un serveur réel (non collectés par pytest - voir leur README) |








