vardrrunner 0.28.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vardrrunner/__init__.py +8 -0
- vardrrunner/api.py +173 -0
- vardrrunner/cli.py +440 -0
- vardrrunner/commands/__init__.py +0 -0
- vardrrunner/commands/auth.py +100 -0
- vardrrunner/commands/daemon.py +326 -0
- vardrrunner/commands/doctor.py +315 -0
- vardrrunner/commands/engagements.py +67 -0
- vardrrunner/commands/heartbeat.py +56 -0
- vardrrunner/commands/imports.py +40 -0
- vardrrunner/commands/jobs.py +177 -0
- vardrrunner/commands/pipeline.py +417 -0
- vardrrunner/commands/run.py +293 -0
- vardrrunner/commands/status.py +113 -0
- vardrrunner/config.py +164 -0
- vardrrunner/configs.py +210 -0
- vardrrunner/handlers.py +498 -0
- vardrrunner/keychain.py +83 -0
- vardrrunner/pipelines.py +48 -0
- vardrrunner/runner.py +389 -0
- vardrrunner/targets.py +76 -0
- vardrrunner-0.28.0.dist-info/METADATA +199 -0
- vardrrunner-0.28.0.dist-info/RECORD +27 -0
- vardrrunner-0.28.0.dist-info/WHEEL +5 -0
- vardrrunner-0.28.0.dist-info/entry_points.txt +2 -0
- vardrrunner-0.28.0.dist-info/licenses/LICENSE +21 -0
- vardrrunner-0.28.0.dist-info/top_level.txt +1 -0
vardrrunner/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""VardrRunner — local automation runner for the VardrSec product family.
|
|
2
|
+
|
|
3
|
+
The version literal below is the single source of truth: `pyproject.toml` reads
|
|
4
|
+
it via setuptools' dynamic-version mechanism, and the heartbeat reports it to the
|
|
5
|
+
backend. Bump it here (and add a CHANGELOG entry) on every release.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.28.0"
|
vardrrunner/api.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thin wrapper around requests for authenticated calls to the VardrMap API.
|
|
3
|
+
All methods raise requests.HTTPError on non-2xx responses.
|
|
4
|
+
|
|
5
|
+
The session retries transient failures (connection errors and 429/5xx) with
|
|
6
|
+
exponential backoff so a long-running daemon survives network blips and brief
|
|
7
|
+
backend restarts. Retries are limited to idempotent methods (urllib3's default:
|
|
8
|
+
GET/HEAD/PUT/DELETE/OPTIONS/TRACE) — POST and PATCH are never auto-retried, so a
|
|
9
|
+
dropped response can't cause a double-claim, double-import, or duplicate event.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import platform
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
from requests.adapters import HTTPAdapter
|
|
17
|
+
from urllib3.util.retry import Retry
|
|
18
|
+
|
|
19
|
+
from vardrrunner import __version__
|
|
20
|
+
|
|
21
|
+
# Statuses worth retrying: rate-limit + transient server/proxy errors.
|
|
22
|
+
_RETRY_STATUSES = (429, 500, 502, 503, 504)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VardrMapClient:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
api_url: str,
|
|
29
|
+
api_key: str,
|
|
30
|
+
*,
|
|
31
|
+
retries: int = 3,
|
|
32
|
+
backoff_factor: float = 0.5,
|
|
33
|
+
):
|
|
34
|
+
self.base = api_url.rstrip("/")
|
|
35
|
+
self.session = requests.Session()
|
|
36
|
+
self.session.headers.update(
|
|
37
|
+
{
|
|
38
|
+
"Authorization": f"Bearer {api_key}",
|
|
39
|
+
# Identify the runner + version in backend logs.
|
|
40
|
+
"User-Agent": f"vardrrunner/{__version__} ({platform.system()})",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Mount a retry-with-backoff adapter for transient failures. allowed_methods
|
|
45
|
+
# is left at urllib3's idempotent-only default so POST/PATCH are not retried.
|
|
46
|
+
retry = Retry(
|
|
47
|
+
total=retries,
|
|
48
|
+
connect=retries,
|
|
49
|
+
read=retries,
|
|
50
|
+
status=retries,
|
|
51
|
+
backoff_factor=backoff_factor,
|
|
52
|
+
status_forcelist=_RETRY_STATUSES,
|
|
53
|
+
raise_on_status=False,
|
|
54
|
+
)
|
|
55
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
56
|
+
self.session.mount("https://", adapter)
|
|
57
|
+
self.session.mount("http://", adapter)
|
|
58
|
+
|
|
59
|
+
def _url(self, path: str) -> str:
|
|
60
|
+
return f"{self.base}/{path.lstrip('/')}"
|
|
61
|
+
|
|
62
|
+
def get(self, path: str, params: dict | None = None) -> Any:
|
|
63
|
+
r = self.session.get(self._url(path), params=params, timeout=30)
|
|
64
|
+
r.raise_for_status()
|
|
65
|
+
return r.json()
|
|
66
|
+
|
|
67
|
+
def post(
|
|
68
|
+
self,
|
|
69
|
+
path: str,
|
|
70
|
+
json: dict | None = None,
|
|
71
|
+
files: dict | None = None,
|
|
72
|
+
data: dict | None = None,
|
|
73
|
+
) -> Any:
|
|
74
|
+
r = self.session.post(self._url(path), json=json, files=files, data=data, timeout=60)
|
|
75
|
+
r.raise_for_status()
|
|
76
|
+
return r.json()
|
|
77
|
+
|
|
78
|
+
def whoami(self) -> dict:
|
|
79
|
+
return self.get("/me")
|
|
80
|
+
|
|
81
|
+
def engagements(self) -> list[dict]:
|
|
82
|
+
data = self.get("/engagements")
|
|
83
|
+
# VardrMap returns both keys during the engagements → engagements
|
|
84
|
+
# transition; prefer the new name and fall back to the old one.
|
|
85
|
+
return data.get("engagements") or data.get("engagements", [])
|
|
86
|
+
|
|
87
|
+
def engagement(self, engagement_id: str) -> dict:
|
|
88
|
+
return self.get(f"/engagements/{engagement_id}")
|
|
89
|
+
|
|
90
|
+
def scope(self, engagement_id: str) -> dict:
|
|
91
|
+
"""Returns {"in": [...], "out": [...]} scope lists."""
|
|
92
|
+
return self.engagement(engagement_id).get("scope", {"in": [], "out": []})
|
|
93
|
+
|
|
94
|
+
# Backend rejects limit values above this with 422.
|
|
95
|
+
RECON_PAGE_SIZE = 500
|
|
96
|
+
|
|
97
|
+
def recon(
|
|
98
|
+
self, engagement_id: str, limit: int = 100, status_code: int | None = None
|
|
99
|
+
) -> list[dict]:
|
|
100
|
+
"""Fetch recon items, paginating in RECON_PAGE_SIZE chunks to avoid backend 422s."""
|
|
101
|
+
results: list[dict] = []
|
|
102
|
+
offset = 0
|
|
103
|
+
page_size = min(limit, self.RECON_PAGE_SIZE)
|
|
104
|
+
|
|
105
|
+
while len(results) < limit:
|
|
106
|
+
remaining = limit - len(results)
|
|
107
|
+
fetch = min(remaining, page_size)
|
|
108
|
+
params: dict = {"limit": fetch, "offset": offset}
|
|
109
|
+
if status_code is not None:
|
|
110
|
+
params["status_code"] = status_code
|
|
111
|
+
page = self.get(f"/engagements/{engagement_id}/recon", params=params).get("recon", [])
|
|
112
|
+
results.extend(page)
|
|
113
|
+
if len(page) < fetch:
|
|
114
|
+
break
|
|
115
|
+
offset += fetch
|
|
116
|
+
|
|
117
|
+
return results
|
|
118
|
+
|
|
119
|
+
def import_file(self, engagement_id: str, tool_type: str, file_path: str) -> dict:
|
|
120
|
+
with open(file_path, "rb") as fh:
|
|
121
|
+
return self.post(
|
|
122
|
+
f"/engagements/{engagement_id}/imports",
|
|
123
|
+
files={"file": (file_path, fh, "application/json")},
|
|
124
|
+
data={"tool_type": tool_type},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
# Scan jobs (job queue for UI-initiated scans)
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
def pending_jobs(self) -> list[dict]:
|
|
132
|
+
"""Return all pending jobs owned by the authenticated user."""
|
|
133
|
+
return self.get("/jobs/pending").get("jobs", [])
|
|
134
|
+
|
|
135
|
+
def claim_job(self, job_id: str) -> dict:
|
|
136
|
+
"""Atomically claim a pending job. Raises HTTPError 409 if already claimed."""
|
|
137
|
+
return self.post(f"/jobs/{job_id}/claim")
|
|
138
|
+
|
|
139
|
+
def complete_job(self, job_id: str, status: str, error: str = "") -> dict:
|
|
140
|
+
"""Mark a job done or failed."""
|
|
141
|
+
payload: dict = {"status": status}
|
|
142
|
+
if error:
|
|
143
|
+
payload["error_message"] = error
|
|
144
|
+
return self.patch(f"/jobs/{job_id}", json=payload)
|
|
145
|
+
|
|
146
|
+
def patch(self, path: str, json: dict | None = None) -> Any:
|
|
147
|
+
r = self.session.patch(self._url(path), json=json, timeout=30)
|
|
148
|
+
r.raise_for_status()
|
|
149
|
+
return r.json()
|
|
150
|
+
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
# Runner heartbeat
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def send_heartbeat(self, payload: dict) -> dict:
|
|
156
|
+
"""Post runner status (hostname, version, os, tools) to the backend."""
|
|
157
|
+
return self.post("/runner/heartbeat", json=payload)
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
# Job events
|
|
161
|
+
# ------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def post_event(self, job_id: str, kind: str, text: str = "") -> dict:
|
|
164
|
+
"""Post a lifecycle event for a job (started, running, done, failed, …)."""
|
|
165
|
+
return self.post(f"/jobs/{job_id}/events", json={"kind": kind, "text": text})
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
# Services
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def create_services(self, engagement_id: str, services: list[dict]) -> dict:
|
|
172
|
+
"""Bulk-upsert nmap service results for an engagement."""
|
|
173
|
+
return self.post(f"/engagements/{engagement_id}/services", json={"services": services})
|
vardrrunner/cli.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
"""
|
|
2
|
+
vardrrunner — local automation runner for the VardrSec product family.
|
|
3
|
+
|
|
4
|
+
Runs security tooling on the operator's machine and syncs results to a VardrSec
|
|
5
|
+
backend (today: VardrMap) over HTTP. See https://github.com/VardrSec/VardrRunner.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from vardrrunner.commands import auth, engagements, imports, jobs, run
|
|
14
|
+
from vardrrunner.commands import daemon as daemon_cmd
|
|
15
|
+
from vardrrunner.commands import doctor as doctor_cmd
|
|
16
|
+
from vardrrunner.commands import heartbeat as heartbeat_cmd
|
|
17
|
+
from vardrrunner.commands import pipeline as pipeline_cmd
|
|
18
|
+
from vardrrunner.commands import status as status_cmd
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
# Shared across every command that resolves targets, so the cap is described
|
|
23
|
+
# identically in `run --help` and `pipeline run --help`.
|
|
24
|
+
_MAX_TARGETS_HELP = "Abort if resolved targets exceed this count (0 disables the cap)"
|
|
25
|
+
|
|
26
|
+
app = typer.Typer(
|
|
27
|
+
name="vardrrunner",
|
|
28
|
+
help="Local runner for VardrMap. Runs tools locally, uploads results to your VardrMap instance.",
|
|
29
|
+
no_args_is_help=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# --------------------------------------------------------------------------- #
|
|
33
|
+
# Auth
|
|
34
|
+
# --------------------------------------------------------------------------- #
|
|
35
|
+
|
|
36
|
+
login_app = typer.Typer(help="Log in to a Vardr product.", no_args_is_help=True)
|
|
37
|
+
app.add_typer(login_app, name="login")
|
|
38
|
+
login_app.command("vardrmap")(auth.login_vardrmap)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def status():
|
|
43
|
+
"""Show config, API connectivity, and local tool availability."""
|
|
44
|
+
status_cmd.run_status()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command()
|
|
48
|
+
def doctor(
|
|
49
|
+
as_json: bool = typer.Option(False, "--json", help="Emit a machine-readable JSON report"),
|
|
50
|
+
):
|
|
51
|
+
"""Deep preflight before unattended use — exits non-zero on actionable failures."""
|
|
52
|
+
doctor_cmd.run_doctor(as_json=as_json)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def heartbeat():
|
|
57
|
+
"""Send a heartbeat to VardrMap — reports hostname, version, and tool status."""
|
|
58
|
+
heartbeat_cmd.send_heartbeat(quiet=False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command()
|
|
62
|
+
def logout():
|
|
63
|
+
"""Remove stored credentials (keychain + config file); keep the API URL."""
|
|
64
|
+
auth.logout()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command()
|
|
68
|
+
def whoami():
|
|
69
|
+
"""Show the identity tied to the configured API key."""
|
|
70
|
+
auth.whoami()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --------------------------------------------------------------------------- #
|
|
74
|
+
# Programs
|
|
75
|
+
# --------------------------------------------------------------------------- #
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@app.command()
|
|
79
|
+
def engagement_list():
|
|
80
|
+
"""List all engagements in VardrMap."""
|
|
81
|
+
engagements.list_engagements()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Alias `engagements` → `engagement-list` for a more natural UX
|
|
85
|
+
app.command(name="engagements")(engagement_list)
|
|
86
|
+
# Retired name, kept so existing habits and scripts do not break. Hidden from
|
|
87
|
+
# --help so only the current name is advertised.
|
|
88
|
+
app.command(name="programs", hidden=True)(engagement_list)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.command()
|
|
92
|
+
def scope(engagement_id: str = typer.Argument(..., help="Engagement UUID")):
|
|
93
|
+
"""Show in-scope and out-of-scope items for an engagement."""
|
|
94
|
+
engagements.show_scope(engagement_id)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --------------------------------------------------------------------------- #
|
|
98
|
+
# Import
|
|
99
|
+
# --------------------------------------------------------------------------- #
|
|
100
|
+
|
|
101
|
+
import_app = typer.Typer(help="Import tool output files into VardrMap.", no_args_is_help=True)
|
|
102
|
+
app.add_typer(import_app, name="import")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@import_app.command("nuclei")
|
|
106
|
+
def import_nuclei(
|
|
107
|
+
engagement_id: str = typer.Option(
|
|
108
|
+
..., "--engagement", "--program", "-p", help="Engagement UUID"
|
|
109
|
+
),
|
|
110
|
+
file: Path = typer.Option(..., "--file", "-f", help="Path to nuclei JSONL output"),
|
|
111
|
+
):
|
|
112
|
+
"""Import a nuclei output file."""
|
|
113
|
+
imports.import_file("nuclei", engagement_id, file)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@import_app.command("httpx")
|
|
117
|
+
def import_httpx(
|
|
118
|
+
engagement_id: str = typer.Option(
|
|
119
|
+
..., "--engagement", "--program", "-p", help="Engagement UUID"
|
|
120
|
+
),
|
|
121
|
+
file: Path = typer.Option(..., "--file", "-f", help="Path to httpx JSON/JSONL output"),
|
|
122
|
+
):
|
|
123
|
+
"""Import an httpx output file."""
|
|
124
|
+
imports.import_file("httpx", engagement_id, file)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --------------------------------------------------------------------------- #
|
|
128
|
+
# Run
|
|
129
|
+
# --------------------------------------------------------------------------- #
|
|
130
|
+
|
|
131
|
+
# --------------------------------------------------------------------------- #
|
|
132
|
+
# Jobs
|
|
133
|
+
# --------------------------------------------------------------------------- #
|
|
134
|
+
|
|
135
|
+
# --------------------------------------------------------------------------- #
|
|
136
|
+
# Daemon
|
|
137
|
+
# --------------------------------------------------------------------------- #
|
|
138
|
+
|
|
139
|
+
daemon_app = typer.Typer(
|
|
140
|
+
help="Long-running background worker: polls jobs and sends heartbeats.", no_args_is_help=True
|
|
141
|
+
)
|
|
142
|
+
app.add_typer(daemon_app, name="daemon")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@daemon_app.command("start")
|
|
146
|
+
def daemon_start(
|
|
147
|
+
detach: bool = typer.Option(False, "--detach", "-d", help="Run in background"),
|
|
148
|
+
poll_interval: int = typer.Option(5, "--poll-interval", help="Seconds between job polls"),
|
|
149
|
+
heartbeat_interval: int = typer.Option(
|
|
150
|
+
60, "--heartbeat-interval", help="Seconds between heartbeats"
|
|
151
|
+
),
|
|
152
|
+
log_file: Path | None = typer.Option(None, "--log-file", help="Append output to file"),
|
|
153
|
+
):
|
|
154
|
+
"""Start the daemon (foreground by default, use --detach for background)."""
|
|
155
|
+
daemon_cmd.start(
|
|
156
|
+
detach=detach,
|
|
157
|
+
poll_interval=poll_interval,
|
|
158
|
+
heartbeat_interval=heartbeat_interval,
|
|
159
|
+
log_file=log_file,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@daemon_app.command("stop")
|
|
164
|
+
def daemon_stop():
|
|
165
|
+
"""Stop a running daemon."""
|
|
166
|
+
daemon_cmd.stop()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@daemon_app.command("status")
|
|
170
|
+
def daemon_status():
|
|
171
|
+
"""Show whether the daemon is running."""
|
|
172
|
+
daemon_cmd.status()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# --------------------------------------------------------------------------- #
|
|
176
|
+
# Jobs
|
|
177
|
+
# --------------------------------------------------------------------------- #
|
|
178
|
+
|
|
179
|
+
jobs_app = typer.Typer(help="Manage and execute scan job queue.", no_args_is_help=True)
|
|
180
|
+
app.add_typer(jobs_app, name="jobs")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@jobs_app.command("list")
|
|
184
|
+
def jobs_list():
|
|
185
|
+
"""List pending scan jobs."""
|
|
186
|
+
jobs.list_jobs()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@jobs_app.command("run")
|
|
190
|
+
def jobs_run(
|
|
191
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts"),
|
|
192
|
+
):
|
|
193
|
+
"""Claim and execute all pending scan jobs."""
|
|
194
|
+
jobs.run_jobs(yes=yes)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# --------------------------------------------------------------------------- #
|
|
198
|
+
# Run
|
|
199
|
+
# --------------------------------------------------------------------------- #
|
|
200
|
+
|
|
201
|
+
run_app = typer.Typer(
|
|
202
|
+
help="Run a tool locally and upload results to VardrMap.", no_args_is_help=True
|
|
203
|
+
)
|
|
204
|
+
app.add_typer(run_app, name="run")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@run_app.command("httpx")
|
|
208
|
+
def run_httpx(
|
|
209
|
+
engagement_id: str = typer.Option(
|
|
210
|
+
..., "--engagement", "--program", "-p", help="Engagement UUID"
|
|
211
|
+
),
|
|
212
|
+
scope: bool = typer.Option(False, "--scope", help="Use in-scope assets from VardrMap"),
|
|
213
|
+
from_recon: bool = typer.Option(
|
|
214
|
+
False, "--from-recon", help="Use live recon items from VardrMap"
|
|
215
|
+
),
|
|
216
|
+
target: str | None = typer.Option(None, "--target", help="Single inline target"),
|
|
217
|
+
targets_file: Path | None = typer.Option(None, "--targets", help="Path to a targets .txt file"),
|
|
218
|
+
limit: int = typer.Option(100, "--limit", help="Max recon items to use (--from-recon only)"),
|
|
219
|
+
status_code: int | None = typer.Option(
|
|
220
|
+
None, "--status-code", help="Filter recon by HTTP status code"
|
|
221
|
+
),
|
|
222
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
223
|
+
max_targets: int = typer.Option(
|
|
224
|
+
run.MAX_TARGETS_DEFAULT, "--max-targets", help=_MAX_TARGETS_HELP
|
|
225
|
+
),
|
|
226
|
+
):
|
|
227
|
+
"""Run httpx locally and upload results to VardrMap."""
|
|
228
|
+
run.run_httpx(
|
|
229
|
+
engagement_id=engagement_id,
|
|
230
|
+
scope=scope,
|
|
231
|
+
from_recon=from_recon,
|
|
232
|
+
target=target,
|
|
233
|
+
targets_file=targets_file,
|
|
234
|
+
limit=limit,
|
|
235
|
+
status_code=status_code,
|
|
236
|
+
yes=yes,
|
|
237
|
+
max_targets=max_targets,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@run_app.command("subfinder")
|
|
242
|
+
def run_subfinder(
|
|
243
|
+
engagement_id: str = typer.Option(
|
|
244
|
+
..., "--engagement", "--program", "-p", help="Engagement UUID"
|
|
245
|
+
),
|
|
246
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
247
|
+
max_targets: int = typer.Option(
|
|
248
|
+
run.MAX_TARGETS_DEFAULT, "--max-targets", help=_MAX_TARGETS_HELP
|
|
249
|
+
),
|
|
250
|
+
):
|
|
251
|
+
"""Run subfinder against wildcard scope entries and import discovered hosts."""
|
|
252
|
+
run.run_subfinder(engagement_id=engagement_id, yes=yes, max_targets=max_targets)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@run_app.command("nuclei")
|
|
256
|
+
def run_nuclei(
|
|
257
|
+
engagement_id: str = typer.Option(
|
|
258
|
+
..., "--engagement", "--program", "-p", help="Engagement UUID"
|
|
259
|
+
),
|
|
260
|
+
scope: bool = typer.Option(False, "--scope", help="Use in-scope assets from VardrMap"),
|
|
261
|
+
from_recon: bool = typer.Option(
|
|
262
|
+
False, "--from-recon", help="Use live recon items from VardrMap"
|
|
263
|
+
),
|
|
264
|
+
target: str | None = typer.Option(None, "--target", help="Single inline target"),
|
|
265
|
+
targets_file: Path | None = typer.Option(None, "--targets", help="Path to a targets .txt file"),
|
|
266
|
+
limit: int = typer.Option(100, "--limit", help="Max recon items to use (--from-recon only)"),
|
|
267
|
+
status_code: int | None = typer.Option(
|
|
268
|
+
None, "--status-code", help="Filter recon by HTTP status code"
|
|
269
|
+
),
|
|
270
|
+
severity: str | None = typer.Option(
|
|
271
|
+
None, "--severity", help="Comma-separated severities, e.g. high,critical"
|
|
272
|
+
),
|
|
273
|
+
templates: str | None = typer.Option(
|
|
274
|
+
None, "--templates", "-t", help="Nuclei template path or tag"
|
|
275
|
+
),
|
|
276
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
277
|
+
max_targets: int = typer.Option(
|
|
278
|
+
run.MAX_TARGETS_DEFAULT, "--max-targets", help=_MAX_TARGETS_HELP
|
|
279
|
+
),
|
|
280
|
+
):
|
|
281
|
+
"""Run nuclei locally and upload results to VardrMap."""
|
|
282
|
+
run.run_nuclei(
|
|
283
|
+
engagement_id=engagement_id,
|
|
284
|
+
scope=scope,
|
|
285
|
+
from_recon=from_recon,
|
|
286
|
+
target=target,
|
|
287
|
+
targets_file=targets_file,
|
|
288
|
+
limit=limit,
|
|
289
|
+
status_code=status_code,
|
|
290
|
+
severity=severity,
|
|
291
|
+
templates=templates,
|
|
292
|
+
yes=yes,
|
|
293
|
+
max_targets=max_targets,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@run_app.command("nmap")
|
|
298
|
+
def run_nmap(
|
|
299
|
+
engagement_id: str = typer.Option(
|
|
300
|
+
..., "--engagement", "--program", "-p", help="Engagement UUID"
|
|
301
|
+
),
|
|
302
|
+
scope: bool = typer.Option(False, "--scope", help="Use in-scope assets from VardrMap"),
|
|
303
|
+
from_recon: bool = typer.Option(
|
|
304
|
+
False, "--from-recon", help="Use live recon items from VardrMap"
|
|
305
|
+
),
|
|
306
|
+
target: str | None = typer.Option(None, "--target", help="Single inline target"),
|
|
307
|
+
targets_file: Path | None = typer.Option(None, "--targets", help="Path to a targets .txt file"),
|
|
308
|
+
limit: int = typer.Option(500, "--limit", help="Max recon items to use (--from-recon only)"),
|
|
309
|
+
top_ports: int = typer.Option(100, "--top-ports", help="Number of most-common ports to scan"),
|
|
310
|
+
timing: int = typer.Option(
|
|
311
|
+
3, "--timing", help="nmap timing template (0-4; 5 is never allowed)"
|
|
312
|
+
),
|
|
313
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
314
|
+
max_targets: int = typer.Option(
|
|
315
|
+
run.MAX_TARGETS_DEFAULT, "--max-targets", help=_MAX_TARGETS_HELP
|
|
316
|
+
),
|
|
317
|
+
):
|
|
318
|
+
"""Run nmap service discovery locally and upload open ports to VardrMap."""
|
|
319
|
+
run.run_nmap(
|
|
320
|
+
engagement_id=engagement_id,
|
|
321
|
+
scope=scope,
|
|
322
|
+
from_recon=from_recon,
|
|
323
|
+
target=target,
|
|
324
|
+
targets_file=targets_file,
|
|
325
|
+
limit=limit,
|
|
326
|
+
top_ports=top_ports,
|
|
327
|
+
timing=timing,
|
|
328
|
+
yes=yes,
|
|
329
|
+
max_targets=max_targets,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@run_app.command("dnsx")
|
|
334
|
+
def run_dnsx(
|
|
335
|
+
engagement_id: str = typer.Option(
|
|
336
|
+
..., "--engagement", "--program", "-p", help="Engagement UUID"
|
|
337
|
+
),
|
|
338
|
+
scope: bool = typer.Option(False, "--scope", help="Use in-scope assets from VardrMap"),
|
|
339
|
+
from_recon: bool = typer.Option(
|
|
340
|
+
False, "--from-recon", help="Use live recon items from VardrMap"
|
|
341
|
+
),
|
|
342
|
+
target: str | None = typer.Option(None, "--target", help="Single inline target"),
|
|
343
|
+
targets_file: Path | None = typer.Option(None, "--targets", help="Path to a targets .txt file"),
|
|
344
|
+
limit: int = typer.Option(500, "--limit", help="Max recon items to use (--from-recon only)"),
|
|
345
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
346
|
+
max_targets: int = typer.Option(
|
|
347
|
+
run.MAX_TARGETS_DEFAULT, "--max-targets", help=_MAX_TARGETS_HELP
|
|
348
|
+
),
|
|
349
|
+
):
|
|
350
|
+
"""Resolve hosts with dnsx and upload the resolvable ones as recon targets."""
|
|
351
|
+
run.run_dnsx(
|
|
352
|
+
engagement_id=engagement_id,
|
|
353
|
+
scope=scope,
|
|
354
|
+
from_recon=from_recon,
|
|
355
|
+
target=target,
|
|
356
|
+
targets_file=targets_file,
|
|
357
|
+
limit=limit,
|
|
358
|
+
yes=yes,
|
|
359
|
+
max_targets=max_targets,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@run_app.command("naabu")
|
|
364
|
+
def run_naabu(
|
|
365
|
+
engagement_id: str = typer.Option(
|
|
366
|
+
..., "--engagement", "--program", "-p", help="Engagement UUID"
|
|
367
|
+
),
|
|
368
|
+
scope: bool = typer.Option(False, "--scope", help="Use in-scope assets from VardrMap"),
|
|
369
|
+
from_recon: bool = typer.Option(
|
|
370
|
+
False, "--from-recon", help="Use live recon items from VardrMap"
|
|
371
|
+
),
|
|
372
|
+
target: str | None = typer.Option(None, "--target", help="Single inline target"),
|
|
373
|
+
targets_file: Path | None = typer.Option(None, "--targets", help="Path to a targets .txt file"),
|
|
374
|
+
limit: int = typer.Option(500, "--limit", help="Max recon items to use (--from-recon only)"),
|
|
375
|
+
top_ports: int = typer.Option(100, "--top-ports", help="Number of most-common ports to scan"),
|
|
376
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
377
|
+
max_targets: int = typer.Option(
|
|
378
|
+
run.MAX_TARGETS_DEFAULT, "--max-targets", help=_MAX_TARGETS_HELP
|
|
379
|
+
),
|
|
380
|
+
):
|
|
381
|
+
"""Port-scan hosts with naabu locally and upload open ports to VardrMap."""
|
|
382
|
+
run.run_naabu(
|
|
383
|
+
engagement_id=engagement_id,
|
|
384
|
+
scope=scope,
|
|
385
|
+
from_recon=from_recon,
|
|
386
|
+
target=target,
|
|
387
|
+
targets_file=targets_file,
|
|
388
|
+
limit=limit,
|
|
389
|
+
top_ports=top_ports,
|
|
390
|
+
yes=yes,
|
|
391
|
+
max_targets=max_targets,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# --------------------------------------------------------------------------- #
|
|
396
|
+
# Pipeline
|
|
397
|
+
# --------------------------------------------------------------------------- #
|
|
398
|
+
|
|
399
|
+
pipeline_app = typer.Typer(help="Run a chain of tools as one recon pipeline.", no_args_is_help=True)
|
|
400
|
+
app.add_typer(pipeline_app, name="pipeline")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@pipeline_app.command("list")
|
|
404
|
+
def pipeline_list():
|
|
405
|
+
"""List the available pipelines and their tool chains."""
|
|
406
|
+
pipeline_cmd.list_pipelines()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@pipeline_app.command("run")
|
|
410
|
+
def pipeline_run(
|
|
411
|
+
name: str = typer.Argument(..., help="Pipeline name (see `pipeline list`)"),
|
|
412
|
+
engagement_id: str = typer.Option(
|
|
413
|
+
..., "--engagement", "--program", "-p", help="Engagement UUID"
|
|
414
|
+
),
|
|
415
|
+
severity: str | None = typer.Option(
|
|
416
|
+
None, "--severity", help="nuclei severity filter for the scan stage"
|
|
417
|
+
),
|
|
418
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
419
|
+
continue_on_error: bool = typer.Option(
|
|
420
|
+
False, "--continue-on-error", help="Keep going if a stage fails"
|
|
421
|
+
),
|
|
422
|
+
dry_run: bool = typer.Option(
|
|
423
|
+
False, "--dry-run", help="Resolve first-stage targets and print the plan without executing"
|
|
424
|
+
),
|
|
425
|
+
as_json: bool = typer.Option(False, "--json", help="Emit a machine-readable JSON result"),
|
|
426
|
+
max_targets: int = typer.Option(
|
|
427
|
+
run.MAX_TARGETS_DEFAULT, "--max-targets", help=_MAX_TARGETS_HELP
|
|
428
|
+
),
|
|
429
|
+
):
|
|
430
|
+
"""Run every stage of a pipeline in order against an engagement."""
|
|
431
|
+
pipeline_cmd.run_pipeline(
|
|
432
|
+
name,
|
|
433
|
+
engagement_id,
|
|
434
|
+
severity=severity,
|
|
435
|
+
yes=yes,
|
|
436
|
+
continue_on_error=continue_on_error,
|
|
437
|
+
dry_run=dry_run,
|
|
438
|
+
as_json=as_json,
|
|
439
|
+
max_targets=max_targets,
|
|
440
|
+
)
|
|
File without changes
|