Skip to content

Commit 26b9cd5

Browse files
LuxVTZclaude
andcommitted
feat: v1.6.0 — Continuous Monitoring + Enhanced AI
Phase 3: Continuous Monitoring (argus monitor): argus monitor example.com --interval 24h --notify argus monitor target.com --interval 1h --preset web --max-runs 10 - MonitorSession: scan → diff_findings() → notify on new → save state - Uses existing incremental.py for new/resolved/unchanged detection - Uses existing notifier.py (Telegram/Discord/Slack) for alerts - Persists MonitorState to ~/.argus-lite/monitors/{id}/state.json - Graceful shutdown on Ctrl+C - CLI shows per-run: risk level, findings count, +new, -resolved Phase 4: Enhanced AI: - Russian language support: ai.language="ru" → full analysis in Russian - RemediationCommand model: finding_title, description, command, platform - System prompt now requests concrete server config snippets (Nginx, Apache, iptables) for each finding - HTML report: new "Remediation Commands" section with code blocks Models: MonitorConfig, MonitorRun, MonitorState, RemediationCommand Tests: 570 pass (11 new monitor tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8ee2de3 commit 26b9cd5

8 files changed

Lines changed: 523 additions & 1 deletion

File tree

src/argus_lite/cli.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,73 @@ def on_fail(target: str, err: str) -> None:
502502
console.print(f" [dim]Summary: {bulk_dir}/summary.html[/dim]")
503503

504504

505+
@main.command("monitor")
506+
@click.argument("target")
507+
@click.option("--interval", default="24h", help="Scan interval (e.g. 1h, 6h, 24h, 7d)")
508+
@click.option("--preset", type=click.Choice(["quick", "full", "web", "recon"]), default="quick")
509+
@click.option("--notify", is_flag=True, default=True, help="Notify on new findings")
510+
@click.option("--max-runs", type=int, default=None, help="Max number of runs (default: infinite)")
511+
def monitor(target: str, interval: str, preset: str, notify: bool, max_runs: int | None) -> None:
512+
"""Continuously monitor a target for new vulnerabilities.
513+
514+
\b
515+
Examples:
516+
argus monitor example.com --interval 24h
517+
argus monitor example.com --interval 1h --preset web --max-runs 10
518+
"""
519+
import asyncio
520+
521+
from argus_lite.core.monitor import MonitorSession
522+
from argus_lite.models.monitor import MonitorConfig
523+
524+
# Parse interval
525+
interval_map = {"h": 3600, "d": 86400, "m": 60}
526+
try:
527+
unit = interval[-1].lower()
528+
num = int(interval[:-1])
529+
seconds = num * interval_map.get(unit, 3600)
530+
except (ValueError, IndexError):
531+
seconds = 86400
532+
533+
console.print(f"[yellow]{LEGAL_NOTICE}[/yellow]")
534+
console.print(f"[bold green]Monitoring: {target}[/bold green]")
535+
console.print(f" Interval: {interval} ({seconds}s) | Preset: {preset}")
536+
if max_runs:
537+
console.print(f" Max runs: {max_runs}")
538+
console.print(f" Notify: {'yes' if notify else 'no'}")
539+
console.print("[dim]Press Ctrl+C to stop[/dim]\n")
540+
541+
config = _get_config()
542+
mc = MonitorConfig(
543+
target=target,
544+
interval_seconds=seconds,
545+
notify_on_new=notify,
546+
max_runs=max_runs,
547+
preset=preset,
548+
)
549+
550+
def on_run(run) -> None:
551+
colors = {"NONE": "green", "LOW": "green", "MEDIUM": "yellow", "HIGH": "red"}
552+
rc = colors.get(run.risk_level, "white")
553+
console.print(
554+
f"[dim]Run {run.run_number}[/dim] | "
555+
f"[{rc}]{run.risk_level}[/{rc}] | "
556+
f"Findings: {run.findings_count} | "
557+
f"[green]+{run.new_count}[/green] new, "
558+
f"[red]-{run.resolved_count}[/red] resolved"
559+
)
560+
561+
session = MonitorSession(mc, config, on_run_complete=on_run)
562+
563+
try:
564+
asyncio.get_event_loop().run_until_complete(session.start())
565+
except KeyboardInterrupt:
566+
console.print("\n[yellow]Monitoring stopped.[/yellow]")
567+
asyncio.get_event_loop().run_until_complete(session.stop())
568+
569+
console.print(f"[dim]Total runs: {len(session._state.runs)}[/dim]")
570+
571+
505572
@main.command("discover")
506573
@click.option("--cve", default=None, help="Find hosts vulnerable to CVE (e.g. CVE-2024-1234)")
507574
@click.option("--tech", default=None, help="Find hosts running technology (e.g. 'WordPress 6.3')")

src/argus_lite/core/ai_analyzer.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"attack_chains": [{"name": "Chain name", "steps": ["Step 1", "Step 2"], "severity": "HIGH/MEDIUM/LOW", "likelihood": "HIGH/MEDIUM/LOW"}],
2323
"prioritized_findings": [{"original_id": "finding-id", "new_priority": 1, "reason": "Why this matters", "exploitability": "EASY/MODERATE/HARD"}],
2424
"recommendations": ["Specific actionable fix 1", "Fix 2"],
25+
"remediation_commands": [{"finding_title": "Missing HSTS", "description": "Add HSTS header", "command": "add_header Strict-Transport-Security \\"max-age=31536000; includeSubDomains\\";", "platform": "nginx"}],
2526
"trend_analysis": "Changes since last scan (if provided)"
2627
}
2728
@@ -30,8 +31,30 @@
3031
- Focus on practical exploitability, not theoretical risk
3132
- This is a passive scanner (info/low severity only) — findings may indicate deeper issues
3233
- Recommendations must be actionable (not "update everything")
34+
- remediation_commands: provide concrete server config snippets (Nginx, Apache, iptables, etc.)
3335
- Do not invent findings that don't exist in the data"""
3436

37+
SYSTEM_PROMPT_RU = """Ты — старший пентестер, анализирующий результаты сканирования безопасности.
38+
Задача: предоставить практические рекомендации на основе данных сканирования.
39+
Отвечай ТОЛЬКО на русском языке.
40+
41+
Отвечай ТОЛЬКО валидным JSON по этой схеме:
42+
{
43+
"executive_summary": "Обзор безопасности на 3-5 предложений",
44+
"attack_chains": [{"name": "Название цепочки", "steps": ["Шаг 1", "Шаг 2"], "severity": "HIGH/MEDIUM/LOW", "likelihood": "HIGH/MEDIUM/LOW"}],
45+
"prioritized_findings": [{"original_id": "finding-id", "new_priority": 1, "reason": "Почему это важно", "exploitability": "EASY/MODERATE/HARD"}],
46+
"recommendations": ["Конкретное действие 1", "Действие 2"],
47+
"remediation_commands": [{"finding_title": "Отсутствует HSTS", "description": "Добавить HSTS заголовок", "command": "add_header Strict-Transport-Security ...", "platform": "nginx"}],
48+
"trend_analysis": "Изменения с прошлого сканирования (если есть)"
49+
}
50+
51+
Правила:
52+
- Будь конкретен к технологическому стеку цели
53+
- Фокусируйся на практической эксплуатируемости
54+
- Рекомендации должны быть действенными
55+
- remediation_commands: предоставляй конкретные конфиги серверов
56+
- Не выдумывай находки, которых нет в данных"""
57+
3558

3659
class AIAnalyzer:
3760
"""Analyzes scan results using an OpenAI-compatible LLM."""
@@ -57,11 +80,14 @@ async def _call_llm(
5780
) -> AIAnalysis:
5881
user_prompt = self._build_user_prompt(scan, previous_scan)
5982

83+
# Choose language-specific system prompt
84+
sys_prompt = SYSTEM_PROMPT_RU if self._config.language == "ru" else SYSTEM_PROMPT
85+
6086
url = f"{self._config.base_url.rstrip('/')}/chat/completions"
6187
payload = {
6288
"model": self._config.model,
6389
"messages": [
64-
{"role": "system", "content": SYSTEM_PROMPT},
90+
{"role": "system", "content": sys_prompt},
6591
{"role": "user", "content": user_prompt},
6692
],
6793
"max_tokens": self._config.max_tokens,

src/argus_lite/core/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ class AIConfig(BaseModel):
107107
api_key: str = ""
108108
max_tokens: int = 4096
109109
timeout: int = 120
110+
language: str = "en" # "en" or "ru" — controls AI response language
110111

111112

112113
class BulkConfig(BaseModel):

src/argus_lite/core/monitor.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Continuous monitoring — repeat scans, diff findings, notify on changes."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
import logging
7+
import uuid
8+
from datetime import datetime, timezone
9+
from pathlib import Path
10+
from typing import Callable
11+
12+
from argus_lite.core.config import AppConfig
13+
from argus_lite.core.incremental import diff_findings
14+
from argus_lite.models.finding import Finding
15+
from argus_lite.models.monitor import MonitorConfig, MonitorRun, MonitorState
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class MonitorSession:
21+
"""Runs repeated scans, diffs findings, notifies on changes."""
22+
23+
def __init__(
24+
self,
25+
monitor_config: MonitorConfig,
26+
app_config: AppConfig,
27+
on_run_complete: Callable[[MonitorRun], None] | None = None,
28+
) -> None:
29+
self._mc = monitor_config
30+
self._app_config = app_config
31+
self._on_run_complete = on_run_complete
32+
self._shutdown = asyncio.Event()
33+
self._run_count = 0
34+
self._state = MonitorState(
35+
monitor_id=str(uuid.uuid4())[:8],
36+
config=monitor_config,
37+
started_at=datetime.now(tz=timezone.utc),
38+
)
39+
40+
async def start(self) -> None:
41+
"""Main monitoring loop. Blocks until stop() or max_runs reached."""
42+
self._state.is_running = True
43+
44+
while not self._shutdown.is_set():
45+
await self._execute_run()
46+
self._run_count += 1
47+
48+
if self._mc.max_runs and self._run_count >= self._mc.max_runs:
49+
break
50+
51+
# Wait for interval or shutdown
52+
try:
53+
await asyncio.wait_for(
54+
self._shutdown.wait(),
55+
timeout=self._mc.interval_seconds,
56+
)
57+
break # shutdown requested
58+
except asyncio.TimeoutError:
59+
continue # interval elapsed
60+
61+
self._state.is_running = False
62+
63+
async def stop(self) -> None:
64+
"""Signal graceful shutdown."""
65+
self._shutdown.set()
66+
67+
async def _execute_run(self) -> None:
68+
"""Single scan cycle: scan → diff → notify → record."""
69+
from argus_lite.core.orchestrator import ScanOrchestrator
70+
from argus_lite.core.risk_scorer import score_scan
71+
72+
logger.info("Monitor run %d for %s", self._run_count + 1, self._mc.target)
73+
74+
orch = ScanOrchestrator(
75+
target=self._mc.target,
76+
config=self._app_config,
77+
preset=self._mc.preset,
78+
skip_cve=True, # Skip CVE in monitor for speed
79+
)
80+
81+
try:
82+
result = await orch.run()
83+
result.risk_summary = score_scan(result)
84+
except Exception as exc:
85+
logger.warning("Monitor scan failed: %s", exc)
86+
run = MonitorRun(
87+
run_number=self._run_count + 1,
88+
timestamp=datetime.now(tz=timezone.utc),
89+
risk_level="NONE",
90+
)
91+
self._state.runs.append(run)
92+
return
93+
94+
# Diff with previous findings
95+
current_findings = result.findings
96+
prev_findings = self._state.last_findings
97+
98+
diff = diff_findings(prev_findings, current_findings)
99+
100+
run = MonitorRun(
101+
run_number=self._run_count + 1,
102+
timestamp=datetime.now(tz=timezone.utc),
103+
scan_id=result.scan_id,
104+
findings_count=len(current_findings),
105+
new_count=len(diff.new),
106+
resolved_count=len(diff.resolved),
107+
unchanged_count=len(diff.unchanged),
108+
risk_level=result.risk_summary.risk_level if result.risk_summary else "NONE",
109+
)
110+
111+
self._state.runs.append(run)
112+
self._state.last_findings = current_findings
113+
114+
# Notify if new findings appeared
115+
if diff.new and self._mc.notify_on_new:
116+
await self._send_notification(result, diff.new)
117+
118+
# Callback
119+
if self._on_run_complete:
120+
try:
121+
self._on_run_complete(run)
122+
except Exception:
123+
pass
124+
125+
# Save state
126+
self._save_state()
127+
128+
async def _send_notification(self, result, new_findings: list[Finding]) -> None:
129+
"""Send notification about new findings."""
130+
try:
131+
from argus_lite.core.notifier import NotificationDispatcher
132+
133+
if not self._app_config.notifications.enabled:
134+
return
135+
136+
dispatcher = NotificationDispatcher(self._app_config.notifications)
137+
if dispatcher.get_active_notifiers():
138+
await dispatcher.notify_all(result)
139+
except Exception as exc:
140+
logger.warning("Monitor notification failed: %s", exc)
141+
142+
def _save_state(self) -> None:
143+
"""Persist monitor state to disk."""
144+
try:
145+
state_dir = (
146+
Path.home() / ".argus-lite" / "monitors" / self._state.monitor_id
147+
)
148+
state_dir.mkdir(parents=True, exist_ok=True)
149+
(state_dir / "state.json").write_text(
150+
self._state.model_dump_json(indent=2)
151+
)
152+
except Exception as exc:
153+
logger.debug("Failed to save monitor state: %s", exc)

src/argus_lite/models/ai.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,23 @@ class PrioritizedFinding(BaseModel):
2323
exploitability: str = "MODERATE" # EASY / MODERATE / HARD
2424

2525

26+
class RemediationCommand(BaseModel):
27+
"""A specific command or config snippet to fix a finding."""
28+
29+
finding_title: str = ""
30+
description: str = ""
31+
command: str = "" # e.g. "add_header X-Frame-Options DENY;"
32+
platform: str = "" # nginx, apache, iptables, etc.
33+
34+
2635
class AIAnalysis(BaseModel):
2736
"""Complete AI analysis of scan results."""
2837

2938
executive_summary: str = ""
3039
attack_chains: list[AttackChain] = []
3140
prioritized_findings: list[PrioritizedFinding] = []
3241
recommendations: list[str] = []
42+
remediation_commands: list[RemediationCommand] = []
3343
trend_analysis: str = ""
3444
model_used: str = ""
3545
tokens_used: int = 0

src/argus_lite/models/monitor.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Continuous monitoring models."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import datetime
6+
7+
from pydantic import BaseModel
8+
9+
from argus_lite.models.finding import Finding
10+
11+
12+
class MonitorConfig(BaseModel):
13+
target: str
14+
interval_seconds: int = 86400
15+
notify_on_new: bool = True
16+
notify_on_resolved: bool = False
17+
max_runs: int | None = None
18+
preset: str = "quick"
19+
20+
21+
class MonitorRun(BaseModel):
22+
run_number: int
23+
timestamp: datetime
24+
scan_id: str = ""
25+
findings_count: int = 0
26+
new_count: int = 0
27+
resolved_count: int = 0
28+
unchanged_count: int = 0
29+
risk_level: str = "NONE"
30+
31+
32+
class MonitorState(BaseModel):
33+
monitor_id: str
34+
config: MonitorConfig
35+
runs: list[MonitorRun] = []
36+
last_findings: list[Finding] = []
37+
started_at: datetime
38+
is_running: bool = False

src/argus_lite/modules/report/html_report.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,16 @@
143143
{% for rec in scan.ai_analysis.recommendations %}<li>{{ rec }}</li>{% endfor %}
144144
</ul>
145145
{% endif %}
146+
{% if scan.ai_analysis.remediation_commands %}
147+
<h3 style="margin-top:16px;font-size:15px;">Remediation Commands</h3>
148+
{% for cmd in scan.ai_analysis.remediation_commands %}
149+
<div style="border:1px solid var(--border);border-radius:8px;padding:12px;margin:8px 0;">
150+
<strong>{{ cmd.finding_title }}</strong> <span style="color:var(--dim);font-size:12px;">{{ cmd.platform }}</span>
151+
<div style="color:var(--dim);font-size:13px;">{{ cmd.description }}</div>
152+
<pre style="background:#1c2128;padding:8px;border-radius:4px;margin-top:6px;font-size:12px;overflow-x:auto;"><code>{{ cmd.command }}</code></pre>
153+
</div>
154+
{% endfor %}
155+
{% endif %}
146156
{% if scan.ai_analysis.trend_analysis %}
147157
<h3 style="margin-top:16px;font-size:15px;">Trend Analysis</h3>
148158
<p style="color:var(--dim);">{{ scan.ai_analysis.trend_analysis }}</p>

0 commit comments

Comments
 (0)