farai 0.1.2 → 0.1.4
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.
- package/dist/cli/index.js +20434 -14443
- package/dist/cli/index.js.map +90 -63
- package/docker/kali/farai-tool-manifest.json +6 -0
- package/docker/kali/farai_mitmproxy_mcp.py +253 -22
- package/package.json +2 -1
- package/src/agent-skills/library/binary-exploitation/SKILL.md +26 -0
- package/src/agent-skills/library/binary-reversing/SKILL.md +24 -0
- package/src/agent-skills/library/crypto-solving/SKILL.md +24 -0
- package/src/agent-skills/library/ctf-solving/SKILL.md +27 -0
- package/src/agent-skills/library/digital-forensics/SKILL.md +24 -0
- package/src/agent-skills/library/ffuf/SKILL.md +4 -4
- package/src/agent-skills/library/nmap/SKILL.md +5 -5
- package/src/agent-skills/library/packet-analysis/SKILL.md +24 -0
- package/src/agent-skills/library/payload-protocol-crafting/SKILL.md +6 -6
- package/src/agent-skills/library/privilege-escalation/SKILL.md +24 -0
- package/src/agent-skills/library/reverse-shells/SKILL.md +8 -9
- package/src/agent-skills/library/searchsploit/SKILL.md +3 -3
- package/src/agent-skills/library/source-security-review/SKILL.md +25 -0
- package/src/agent-skills/library/web-assessment/SKILL.md +28 -0
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"hashcat",
|
|
34
34
|
"hydra",
|
|
35
35
|
"impacket-scripts",
|
|
36
|
+
"imagemagick",
|
|
36
37
|
"iproute2",
|
|
37
38
|
"iptables",
|
|
38
39
|
"iputils-ping",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"nuclei",
|
|
57
58
|
"openssh-client",
|
|
58
59
|
"openssl",
|
|
60
|
+
"poppler-utils",
|
|
59
61
|
"pipx",
|
|
60
62
|
"procps",
|
|
61
63
|
"python3",
|
|
@@ -75,6 +77,7 @@
|
|
|
75
77
|
"steghide",
|
|
76
78
|
"subfinder",
|
|
77
79
|
"tcpdump",
|
|
80
|
+
"tesseract-ocr",
|
|
78
81
|
"testssl.sh",
|
|
79
82
|
"theharvester",
|
|
80
83
|
"tree",
|
|
@@ -98,14 +101,17 @@
|
|
|
98
101
|
"curl",
|
|
99
102
|
"dig",
|
|
100
103
|
"file",
|
|
104
|
+
"identify",
|
|
101
105
|
"ip",
|
|
102
106
|
"iptables",
|
|
103
107
|
"jq",
|
|
104
108
|
"nc",
|
|
105
109
|
"openssl",
|
|
110
|
+
"pdftotext",
|
|
106
111
|
"ping",
|
|
107
112
|
"rg",
|
|
108
113
|
"ssh",
|
|
114
|
+
"tesseract",
|
|
109
115
|
"unzip",
|
|
110
116
|
"wget",
|
|
111
117
|
"whois",
|
|
@@ -5,8 +5,10 @@ from __future__ import annotations
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
import sqlite3
|
|
9
10
|
import sys
|
|
11
|
+
import uuid
|
|
10
12
|
from datetime import datetime, timezone
|
|
11
13
|
from typing import Any
|
|
12
14
|
|
|
@@ -28,15 +30,16 @@ def env_int(name: str, default: int, minimum: int, maximum: int) -> int:
|
|
|
28
30
|
MAX_BODY_PREVIEW = env_int("FARAI_PROXY_BODY_PREVIEW_BYTES", 65536, 1024, 1024 * 1024)
|
|
29
31
|
MAX_MESSAGE_PREVIEW = env_int("FARAI_PROXY_MESSAGE_PREVIEW_BYTES", 16384, 1024, 256 * 1024)
|
|
30
32
|
MAX_MESSAGES = env_int("FARAI_PROXY_MAX_MESSAGES", 200, 1, 1000)
|
|
33
|
+
REPLAY_CORRELATION_HEADER = "X-Farai-Replay-Correlation"
|
|
31
34
|
|
|
32
35
|
|
|
33
|
-
class
|
|
36
|
+
class FlowStore:
|
|
34
37
|
def __init__(self, db_path: str):
|
|
35
38
|
self.db_path = db_path
|
|
36
39
|
with self._connect() as conn:
|
|
37
40
|
conn.execute(
|
|
38
41
|
"""
|
|
39
|
-
CREATE TABLE IF NOT EXISTS
|
|
42
|
+
CREATE TABLE IF NOT EXISTS farai_flows (
|
|
40
43
|
id TEXT PRIMARY KEY,
|
|
41
44
|
kind TEXT NOT NULL,
|
|
42
45
|
timestamp REAL NOT NULL,
|
|
@@ -46,12 +49,12 @@ class FlowStoreV2:
|
|
|
46
49
|
"""
|
|
47
50
|
)
|
|
48
51
|
conn.execute(
|
|
49
|
-
"CREATE INDEX IF NOT EXISTS
|
|
50
|
-
"ON
|
|
52
|
+
"CREATE INDEX IF NOT EXISTS idx_farai_flows_timestamp "
|
|
53
|
+
"ON farai_flows(timestamp)"
|
|
51
54
|
)
|
|
52
55
|
conn.execute(
|
|
53
|
-
"CREATE INDEX IF NOT EXISTS
|
|
54
|
-
"ON
|
|
56
|
+
"CREATE INDEX IF NOT EXISTS idx_farai_flows_kind "
|
|
57
|
+
"ON farai_flows(kind)"
|
|
55
58
|
)
|
|
56
59
|
|
|
57
60
|
def _connect(self) -> sqlite3.Connection:
|
|
@@ -65,7 +68,7 @@ class FlowStoreV2:
|
|
|
65
68
|
with self._connect() as conn:
|
|
66
69
|
conn.execute(
|
|
67
70
|
"""
|
|
68
|
-
INSERT INTO
|
|
71
|
+
INSERT INTO farai_flows(id, kind, timestamp, summary_json, detail_json)
|
|
69
72
|
VALUES (?, ?, ?, ?, ?)
|
|
70
73
|
ON CONFLICT(id) DO UPDATE SET
|
|
71
74
|
kind=excluded.kind,
|
|
@@ -78,7 +81,7 @@ class FlowStoreV2:
|
|
|
78
81
|
|
|
79
82
|
def summaries(self, limit: int, kind: str | None = None) -> list[dict[str, Any]]:
|
|
80
83
|
bounded_limit = max(1, min(int(limit), 1000))
|
|
81
|
-
sql = "SELECT summary_json FROM
|
|
84
|
+
sql = "SELECT summary_json FROM farai_flows"
|
|
82
85
|
params: list[Any] = []
|
|
83
86
|
if kind:
|
|
84
87
|
sql += " WHERE kind = ?"
|
|
@@ -92,22 +95,42 @@ class FlowStoreV2:
|
|
|
92
95
|
def detail(self, flow_id: str) -> dict[str, Any] | None:
|
|
93
96
|
with self._connect() as conn:
|
|
94
97
|
row = conn.execute(
|
|
95
|
-
"SELECT detail_json FROM
|
|
98
|
+
"SELECT detail_json FROM farai_flows WHERE id = ?", (flow_id,)
|
|
96
99
|
).fetchone()
|
|
97
100
|
return json.loads(row[0]) if row else None
|
|
98
101
|
|
|
99
102
|
def clear(self) -> None:
|
|
100
103
|
with self._connect() as conn:
|
|
101
|
-
conn.execute("DELETE FROM
|
|
104
|
+
conn.execute("DELETE FROM farai_flows")
|
|
102
105
|
|
|
103
106
|
|
|
104
107
|
class FaraiTrafficRecorder(UPSTREAM_TRAFFIC_RECORDER):
|
|
105
108
|
def __init__(self, scope: Any):
|
|
106
109
|
super().__init__(scope)
|
|
107
|
-
self.
|
|
110
|
+
self.flows = FlowStore(self.db.db_path)
|
|
111
|
+
self.intercept_config: dict[str, Any] = {
|
|
112
|
+
"enabled": False,
|
|
113
|
+
"host_pattern": ".*",
|
|
114
|
+
"path_pattern": ".*",
|
|
115
|
+
"methods": [],
|
|
116
|
+
}
|
|
117
|
+
self.pending_intercepts: dict[str, http.HTTPFlow] = {}
|
|
118
|
+
self.replay_correlations: dict[str, dict[str, str]] = {}
|
|
108
119
|
|
|
109
120
|
def request(self, flow: http.HTTPFlow) -> None:
|
|
121
|
+
correlation = flow.request.headers.get(REPLAY_CORRELATION_HEADER)
|
|
122
|
+
if correlation:
|
|
123
|
+
flow.request.headers.pop(REPLAY_CORRELATION_HEADER, None)
|
|
124
|
+
replay = self.replay_correlations.get(str(correlation))
|
|
125
|
+
if replay is not None:
|
|
126
|
+
replay["descendantFlowId"] = flow.id
|
|
127
|
+
metadata = getattr(flow, "metadata", None)
|
|
128
|
+
if isinstance(metadata, dict):
|
|
129
|
+
metadata["faraiReplayParentId"] = replay["parentFlowId"]
|
|
110
130
|
super().request(flow)
|
|
131
|
+
if self._should_intercept(flow):
|
|
132
|
+
flow.intercept()
|
|
133
|
+
self.pending_intercepts[flow.id] = flow
|
|
111
134
|
self._capture(flow)
|
|
112
135
|
|
|
113
136
|
def response(self, flow: http.HTTPFlow) -> None:
|
|
@@ -160,24 +183,106 @@ class FaraiTrafficRecorder(UPSTREAM_TRAFFIC_RECORDER):
|
|
|
160
183
|
def dns_error(self, flow: dns.DNSFlow) -> None:
|
|
161
184
|
self._capture(flow)
|
|
162
185
|
|
|
163
|
-
def
|
|
164
|
-
return self.
|
|
186
|
+
def farai_flow_summaries(self, limit: int = 20, kind: str | None = None) -> list[dict[str, Any]]:
|
|
187
|
+
return self.flows.summaries(limit, kind)
|
|
165
188
|
|
|
166
|
-
def
|
|
167
|
-
return self.
|
|
189
|
+
def farai_flow_detail(self, flow_id: str) -> dict[str, Any] | None:
|
|
190
|
+
return self.flows.detail(flow_id)
|
|
168
191
|
|
|
169
192
|
def clear(self) -> None:
|
|
170
193
|
super().clear()
|
|
171
|
-
self.
|
|
194
|
+
self.flows.clear()
|
|
195
|
+
|
|
196
|
+
def scope_state(self) -> dict[str, Any]:
|
|
197
|
+
return {"allowedDomains": list(self.scope.config.allowed_domains)}
|
|
198
|
+
|
|
199
|
+
def configure_intercept(
|
|
200
|
+
self,
|
|
201
|
+
enabled: bool,
|
|
202
|
+
host_pattern: str = ".*",
|
|
203
|
+
path_pattern: str = ".*",
|
|
204
|
+
methods: list[str] | None = None,
|
|
205
|
+
) -> dict[str, Any]:
|
|
206
|
+
re.compile(host_pattern)
|
|
207
|
+
re.compile(path_pattern)
|
|
208
|
+
self.intercept_config = {
|
|
209
|
+
"enabled": bool(enabled),
|
|
210
|
+
"host_pattern": host_pattern,
|
|
211
|
+
"path_pattern": path_pattern,
|
|
212
|
+
"methods": sorted({method.upper() for method in (methods or [])}),
|
|
213
|
+
}
|
|
214
|
+
return {**self.intercept_config, "pending": len(self.pending_intercepts)}
|
|
215
|
+
|
|
216
|
+
def list_intercepts(self) -> list[dict[str, Any]]:
|
|
217
|
+
result: list[dict[str, Any]] = []
|
|
218
|
+
for flow in self.pending_intercepts.values():
|
|
219
|
+
summary, _, _ = serialize_http(flow)
|
|
220
|
+
result.append(summary)
|
|
221
|
+
return sorted(result, key=lambda item: str(item.get("timestamp", "")))
|
|
222
|
+
|
|
223
|
+
def begin_replay(self, parent_flow_id: str) -> tuple[str, dict[str, str]]:
|
|
224
|
+
token = uuid.uuid4().hex
|
|
225
|
+
state = {"parentFlowId": parent_flow_id}
|
|
226
|
+
self.replay_correlations[token] = state
|
|
227
|
+
return token, state
|
|
228
|
+
|
|
229
|
+
def finish_replay(self, token: str) -> dict[str, str]:
|
|
230
|
+
return self.replay_correlations.pop(token, {})
|
|
231
|
+
|
|
232
|
+
def resolve_intercept(
|
|
233
|
+
self,
|
|
234
|
+
flow_id: str,
|
|
235
|
+
action: str,
|
|
236
|
+
method: str | None = None,
|
|
237
|
+
url: str | None = None,
|
|
238
|
+
headers: dict[str, str] | None = None,
|
|
239
|
+
body: str | None = None,
|
|
240
|
+
) -> dict[str, Any]:
|
|
241
|
+
flow = self.pending_intercepts.pop(flow_id, None)
|
|
242
|
+
if flow is None:
|
|
243
|
+
raise ValueError(f"Unknown pending intercepted flow: {flow_id}")
|
|
244
|
+
if action == "drop":
|
|
245
|
+
flow.kill()
|
|
246
|
+
return {"flowId": flow_id, "action": "drop"}
|
|
247
|
+
if action not in {"forward", "edit"}:
|
|
248
|
+
self.pending_intercepts[flow_id] = flow
|
|
249
|
+
raise ValueError("action must be forward, edit, or drop")
|
|
250
|
+
if action == "edit":
|
|
251
|
+
if method:
|
|
252
|
+
flow.request.method = method.upper()
|
|
253
|
+
if url:
|
|
254
|
+
flow.request.url = url
|
|
255
|
+
for key, value in (headers or {}).items():
|
|
256
|
+
flow.request.headers[key] = value
|
|
257
|
+
if body is not None:
|
|
258
|
+
flow.request.text = body
|
|
259
|
+
flow.resume()
|
|
260
|
+
return {"flowId": flow_id, "action": action}
|
|
261
|
+
|
|
262
|
+
def _should_intercept(self, flow: http.HTTPFlow) -> bool:
|
|
263
|
+
config = self.intercept_config
|
|
264
|
+
if not config["enabled"] or flow.id in self.pending_intercepts:
|
|
265
|
+
return False
|
|
266
|
+
methods = config["methods"]
|
|
267
|
+
return (
|
|
268
|
+
(not methods or flow.request.method.upper() in methods)
|
|
269
|
+
and re.search(config["host_pattern"], http_display_host(flow.request)) is not None
|
|
270
|
+
and re.search(config["path_pattern"], flow.request.path) is not None
|
|
271
|
+
)
|
|
172
272
|
|
|
173
273
|
def _capture(self, flow: Any) -> None:
|
|
174
274
|
try:
|
|
175
275
|
if not self._is_allowed(flow):
|
|
176
276
|
return
|
|
177
277
|
summary, detail, timestamp = serialize_flow(flow)
|
|
178
|
-
|
|
278
|
+
metadata = getattr(flow, "metadata", None)
|
|
279
|
+
parent_flow_id = metadata.get("faraiReplayParentId") if isinstance(metadata, dict) else None
|
|
280
|
+
if parent_flow_id:
|
|
281
|
+
summary["parentFlowId"] = parent_flow_id
|
|
282
|
+
detail["parentFlowId"] = parent_flow_id
|
|
283
|
+
self.flows.save(summary, detail, timestamp)
|
|
179
284
|
except Exception as exc:
|
|
180
|
-
print(f"Failed to save Farai
|
|
285
|
+
print(f"Failed to save Farai flow: {exc}", file=sys.stderr)
|
|
181
286
|
|
|
182
287
|
def _is_allowed(self, flow: Any) -> bool:
|
|
183
288
|
if isinstance(flow, http.HTTPFlow):
|
|
@@ -537,24 +642,150 @@ def optional(target: dict[str, Any], key: str, value: Any) -> None:
|
|
|
537
642
|
|
|
538
643
|
|
|
539
644
|
@server.mcp.tool()
|
|
540
|
-
async def
|
|
645
|
+
async def proxy_flow_summaries(limit: int = 20, kind: str | None = None) -> str:
|
|
541
646
|
"""Return HTTP, WebSocket, TCP, UDP, and DNS flow summaries."""
|
|
542
647
|
recorder = server.controller.recorder
|
|
543
648
|
if not isinstance(recorder, FaraiTrafficRecorder):
|
|
544
649
|
return "[]"
|
|
545
|
-
return json.dumps(recorder.
|
|
650
|
+
return json.dumps(recorder.farai_flow_summaries(limit, kind), indent=2)
|
|
546
651
|
|
|
547
652
|
|
|
548
653
|
@server.mcp.tool()
|
|
549
|
-
async def
|
|
654
|
+
async def proxy_flow_inspect(flow_id: str) -> str:
|
|
550
655
|
"""Return protocol-aware detail for a captured flow."""
|
|
551
656
|
recorder = server.controller.recorder
|
|
552
657
|
if not isinstance(recorder, FaraiTrafficRecorder):
|
|
553
658
|
return "Couldn't find that flow."
|
|
554
|
-
detail = recorder.
|
|
659
|
+
detail = recorder.farai_flow_detail(flow_id)
|
|
555
660
|
return json.dumps(detail, indent=2) if detail else "Couldn't find that flow."
|
|
556
661
|
|
|
557
662
|
|
|
663
|
+
@server.mcp.tool()
|
|
664
|
+
async def proxy_replay_correlated(
|
|
665
|
+
flow_id: str,
|
|
666
|
+
method: str | None = None,
|
|
667
|
+
headers_json: str | None = None,
|
|
668
|
+
body: str | None = None,
|
|
669
|
+
timeout: float = 30.0,
|
|
670
|
+
) -> str:
|
|
671
|
+
"""Replay an HTTP flow and return its exact captured descendant flow id."""
|
|
672
|
+
recorder = server.controller.recorder
|
|
673
|
+
if not isinstance(recorder, FaraiTrafficRecorder):
|
|
674
|
+
return json.dumps({"ok": False, "error": "Farai recorder is unavailable."})
|
|
675
|
+
if recorder.farai_flow_detail(flow_id) is None:
|
|
676
|
+
return json.dumps({"ok": False, "error": f"Couldn't find flow {flow_id}."})
|
|
677
|
+
try:
|
|
678
|
+
headers = json.loads(headers_json) if headers_json else {}
|
|
679
|
+
except json.JSONDecodeError as exc:
|
|
680
|
+
return json.dumps({"ok": False, "error": f"Invalid headers_json: {exc}"})
|
|
681
|
+
if not isinstance(headers, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in headers.items()):
|
|
682
|
+
return json.dumps({"ok": False, "error": "headers_json must encode a string-to-string object."})
|
|
683
|
+
|
|
684
|
+
# Upstream treats None as "reuse the captured body"; an empty string omits it.
|
|
685
|
+
resolved_body = "" if body == "__omit__" else body
|
|
686
|
+
variables = getattr(server.controller, "session_variables", {})
|
|
687
|
+
resolver = getattr(server, "_resolve_template", None)
|
|
688
|
+
if variables and callable(resolver):
|
|
689
|
+
headers = json.loads(resolver(json.dumps(headers), variables))
|
|
690
|
+
if resolved_body:
|
|
691
|
+
resolved_body = resolver(resolved_body, variables)
|
|
692
|
+
|
|
693
|
+
token, correlation = recorder.begin_replay(flow_id)
|
|
694
|
+
headers[REPLAY_CORRELATION_HEADER] = token
|
|
695
|
+
try:
|
|
696
|
+
message = await server.controller.replay_request(flow_id, method, headers, resolved_body, timeout)
|
|
697
|
+
result = recorder.finish_replay(token)
|
|
698
|
+
except Exception as exc:
|
|
699
|
+
recorder.finish_replay(token)
|
|
700
|
+
return json.dumps({"ok": False, "parentFlowId": flow_id, "error": str(exc)})
|
|
701
|
+
ok = not any(marker in message.lower() for marker in ("couldn't find", "didn't work", "invalid"))
|
|
702
|
+
return json.dumps(
|
|
703
|
+
{
|
|
704
|
+
"ok": ok,
|
|
705
|
+
**correlation,
|
|
706
|
+
**result,
|
|
707
|
+
"message": message,
|
|
708
|
+
**({} if ok else {"error": message}),
|
|
709
|
+
},
|
|
710
|
+
indent=2,
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
@server.mcp.tool()
|
|
715
|
+
async def proxy_scope_get() -> str:
|
|
716
|
+
"""Return Farai's active mitmproxy capture scope."""
|
|
717
|
+
recorder = server.controller.recorder
|
|
718
|
+
if not isinstance(recorder, FaraiTrafficRecorder):
|
|
719
|
+
return json.dumps({"allowedDomains": []})
|
|
720
|
+
return json.dumps(recorder.scope_state(), indent=2)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
@server.mcp.tool()
|
|
724
|
+
async def proxy_intercept_configure(
|
|
725
|
+
enabled: bool,
|
|
726
|
+
host_pattern: str = ".*",
|
|
727
|
+
path_pattern: str = ".*",
|
|
728
|
+
methods: list[str] | None = None,
|
|
729
|
+
) -> str:
|
|
730
|
+
"""Enable or disable Farai's manual request interception queue."""
|
|
731
|
+
recorder = server.controller.recorder
|
|
732
|
+
if not isinstance(recorder, FaraiTrafficRecorder):
|
|
733
|
+
raise RuntimeError("Farai recorder is unavailable.")
|
|
734
|
+
try:
|
|
735
|
+
return json.dumps(
|
|
736
|
+
recorder.configure_intercept(enabled, host_pattern, path_pattern, methods),
|
|
737
|
+
indent=2,
|
|
738
|
+
)
|
|
739
|
+
except re.error as exc:
|
|
740
|
+
raise ValueError(f"Invalid interception pattern: {exc}") from exc
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
@server.mcp.tool()
|
|
744
|
+
async def proxy_intercept_get() -> str:
|
|
745
|
+
"""Return Farai's manual interception configuration and queue size."""
|
|
746
|
+
recorder = server.controller.recorder
|
|
747
|
+
if not isinstance(recorder, FaraiTrafficRecorder):
|
|
748
|
+
return json.dumps({"enabled": False, "pending": 0})
|
|
749
|
+
return json.dumps(
|
|
750
|
+
{**recorder.intercept_config, "pending": len(recorder.pending_intercepts)},
|
|
751
|
+
indent=2,
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
@server.mcp.tool()
|
|
756
|
+
async def proxy_intercept_list() -> str:
|
|
757
|
+
"""List requests currently paused in Farai's interception queue."""
|
|
758
|
+
recorder = server.controller.recorder
|
|
759
|
+
if not isinstance(recorder, FaraiTrafficRecorder):
|
|
760
|
+
return "[]"
|
|
761
|
+
return json.dumps(recorder.list_intercepts(), indent=2)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
@server.mcp.tool()
|
|
765
|
+
async def proxy_intercept_resolve(
|
|
766
|
+
flow_id: str,
|
|
767
|
+
action: str,
|
|
768
|
+
method: str | None = None,
|
|
769
|
+
url: str | None = None,
|
|
770
|
+
headers_json: str | None = None,
|
|
771
|
+
body: str | None = None,
|
|
772
|
+
) -> str:
|
|
773
|
+
"""Forward, edit, or drop one request from Farai's interception queue."""
|
|
774
|
+
recorder = server.controller.recorder
|
|
775
|
+
if not isinstance(recorder, FaraiTrafficRecorder):
|
|
776
|
+
raise RuntimeError("Farai recorder is unavailable.")
|
|
777
|
+
headers = json.loads(headers_json) if headers_json else None
|
|
778
|
+
if headers is not None and (
|
|
779
|
+
not isinstance(headers, dict)
|
|
780
|
+
or not all(isinstance(key, str) and isinstance(value, str) for key, value in headers.items())
|
|
781
|
+
):
|
|
782
|
+
raise ValueError("headers_json must encode a string-to-string object.")
|
|
783
|
+
return json.dumps(
|
|
784
|
+
recorder.resolve_intercept(flow_id, action, method, url, headers, body),
|
|
785
|
+
indent=2,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
|
|
558
789
|
server.TrafficRecorder = FaraiTrafficRecorder
|
|
559
790
|
|
|
560
791
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "farai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Cyber-first freestyle local AI agent",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@opentui/core": "0.5.8",
|
|
63
|
+
"ajv": "8.20.0",
|
|
63
64
|
"bun-pty": "0.4.10",
|
|
64
65
|
"entities": "7.0.1",
|
|
65
66
|
"fuzzysort": "3.1.0",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: binary-exploitation
|
|
3
|
+
description: "Workflow for pwn and memory-corruption challenges: interface discovery, mitigations, crash triage, primitive construction, local exploit validation, and remote adaptation. Use for buffer overflows, format strings, heap bugs, ROP, shellcode, race exploitation, or exploit development against a supplied binary/service."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# binary exploitation
|
|
7
|
+
|
|
8
|
+
build and validate one exploitation primitive at a time.
|
|
9
|
+
|
|
10
|
+
1. identify architecture, mitigations, linkage, libc/loader information, protocol or menu behavior, and how input reaches the process.
|
|
11
|
+
2. reproduce a controlled failure locally. determine the exact offset and root cause before building a payload around it.
|
|
12
|
+
3. name the primitive you have and the next primitive required: instruction-pointer control, leak, arbitrary read/write, allocation control, stack pivot, or code execution.
|
|
13
|
+
4. construct the smallest payload that proves each transition. inspect registers, memory, stack alignment, bad bytes, and process state rather than inferring success from a disconnect.
|
|
14
|
+
5. script the interaction early enough to make attempts deterministic, but keep payload stages observable while debugging.
|
|
15
|
+
6. only after the local path is stable, account for remote differences such as libc, loader, ASLR, buffering, timing, file descriptors, and network framing.
|
|
16
|
+
|
|
17
|
+
## recovery
|
|
18
|
+
|
|
19
|
+
- inconsistent offset: verify the exact input path, newline handling, encoding, and crash context.
|
|
20
|
+
- gadget or address failure: re-check architecture, module base, PIE, stack alignment, and bad bytes.
|
|
21
|
+
- local success but remote failure: preserve the local proof, then isolate one environmental difference at a time.
|
|
22
|
+
- no obvious memory bug: return to input boundaries and state transitions; do not force a ROP approach onto a logic or crypto challenge.
|
|
23
|
+
|
|
24
|
+
## completion
|
|
25
|
+
|
|
26
|
+
confirm the requested effect through process behavior or recovered output. a crash, probable gadget chain, or unexplained connection close is not proof of exploitation.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: binary-reversing
|
|
3
|
+
description: Workflow for understanding native, managed, Go, Rust, JVM, Android, firmware, or obfuscated binaries and recovering hidden logic or data. Use for reverse-engineering challenges, crackmes, malware logic, decompilation, key/flag recovery, or when source is unavailable; pair with binary-exploitation only when memory corruption is the objective.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# binary reversing
|
|
7
|
+
|
|
8
|
+
move from cheap structural facts to the smallest control-flow slice that answers the objective.
|
|
9
|
+
|
|
10
|
+
1. identify format, architecture, linkage, symbols, protections, runtime, imports, sections, and obvious embedded artifacts. record facts before interpreting them.
|
|
11
|
+
2. run the program with bounded, representative inputs when safe and useful. observe files, arguments, environment, stdout/stderr, and system interactions instead of guessing its interface from strings alone.
|
|
12
|
+
3. locate the success, comparison, decryption, parsing, or output path. work backward through callers and data references; avoid decompiling the entire binary without a question.
|
|
13
|
+
4. reconcile decompiler output with disassembly or runtime state whenever types, compiler optimizations, obfuscation, or stripped symbols make the pseudocode ambiguous.
|
|
14
|
+
5. extract constants and encode the recovered transformation in a small script. validate it against known program behavior or by round-tripping an input.
|
|
15
|
+
|
|
16
|
+
## runtime-aware guidance
|
|
17
|
+
|
|
18
|
+
- Go/Rust/managed binaries often retain metadata that is more useful than generic string scraping; use runtime-aware symbols and metadata before treating the file as ordinary stripped native code.
|
|
19
|
+
- packed or self-modifying code may require observing the unpacked memory image before static analysis becomes meaningful.
|
|
20
|
+
- when execution is silent, inspect exit status and side effects and trace the relevant path; do not repeatedly rerun with random input.
|
|
21
|
+
|
|
22
|
+
## completion
|
|
23
|
+
|
|
24
|
+
the result is complete when the recovered logic or data is independently reproduced, not when a likely function or interesting string has merely been identified.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crypto-solving
|
|
3
|
+
description: "Workflow for cryptography challenges and custom encoding: formalize the construction, identify exploitable structure, implement attacks, and validate recovered plaintext or keys. Use for classical ciphers, RSA/ECC mistakes, symmetric misuse, PRNG attacks, hashes/MACs, secret sharing, lattice-style tasks, or layered encodings."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# crypto solving
|
|
7
|
+
|
|
8
|
+
model the exact construction before choosing an attack.
|
|
9
|
+
|
|
10
|
+
1. extract all known values, unknowns, equations, encodings, lengths, randomness assumptions, and attacker capabilities from source and output. preserve byte-versus-text and endian boundaries.
|
|
11
|
+
2. identify the primitive and the implementation weakness separately. the name of a cipher does not establish the vulnerability.
|
|
12
|
+
3. test the simplest structural explanation first: classical transformation, reused nonce/keystream, weak randomness, small parameter, oracle, algebraic relation, truncation, or encoding confusion.
|
|
13
|
+
4. create a small script that reproduces the known output before using it to recover unknown values. validate intermediate equations on toy or supplied samples.
|
|
14
|
+
5. use brute force only after bounding the search space and exploiting every available constraint. report the actual tested space and stopping condition.
|
|
15
|
+
|
|
16
|
+
## recovery
|
|
17
|
+
|
|
18
|
+
- plausible but unreadable plaintext: re-check alphabet, offsets, block boundaries, padding, endian order, and whether another encoding layer remains.
|
|
19
|
+
- attack almost works: compare the implementation line by line with the mathematical assumption; challenge code often differs from the standard primitive in one decisive detail.
|
|
20
|
+
- multiple candidates: use format-independent constraints or re-encryption, not an assumed flag prefix alone, to select the answer.
|
|
21
|
+
|
|
22
|
+
## completion
|
|
23
|
+
|
|
24
|
+
validate recovered plaintext, key material, or forged output by reversing the construction, re-encrypting, or satisfying the challenge verifier whenever possible.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ctf-solving
|
|
3
|
+
description: End-to-end workflow for solving CTF challenges and benchmark tasks across web, crypto, pwn, reversing, forensics, and misc. Use when the user asks to solve a challenge, recover a flag, or continue a stalled challenge run; combine it with one domain skill when the category is known.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ctf solving
|
|
7
|
+
|
|
8
|
+
drive the challenge to a validated answer, not merely an analysis report.
|
|
9
|
+
|
|
10
|
+
1. identify the exact objective, supplied artifacts, reachable targets, constraints, and current working directory. verify that required files or services are actually accessible before analyzing them.
|
|
11
|
+
2. classify the dominant domain from evidence, then load at most the relevant specialist skill. a mixed challenge may change domains later; do not preload every playbook.
|
|
12
|
+
3. establish one concrete hypothesis and run the cheapest discriminating test. preserve useful artifacts, commands, decoded values, offsets, endpoints, and credentials as the solve progresses.
|
|
13
|
+
4. when a path fails, explain what the result ruled out and change the method. do not spend the run repeating filesystem searches, scanners, decoders, or equivalent payload variants.
|
|
14
|
+
5. automate repetitive transformations or interaction once the manual primitive is understood. keep scripts in the workspace when they are part of the reproducible solve.
|
|
15
|
+
6. continue past reconnaissance and partial reverse engineering until the requested objective is reached or a specific missing dependency blocks progress.
|
|
16
|
+
|
|
17
|
+
## completion
|
|
18
|
+
|
|
19
|
+
- validate the answer against the challenge behavior or available oracle when possible.
|
|
20
|
+
- do not assume a fixed flag prefix or format.
|
|
21
|
+
- report the answer first, then the shortest reproducible explanation and any remaining uncertainty.
|
|
22
|
+
|
|
23
|
+
## recovery
|
|
24
|
+
|
|
25
|
+
- missing artifact: verify the manifest, workspace mapping, archive contents, and target lifecycle once; report the exact missing input instead of searching the entire system repeatedly.
|
|
26
|
+
- insufficient tooling: use an available Kali alternative or write a focused parser/script rather than stopping at a tool name.
|
|
27
|
+
- remote/local mismatch: compare architecture, libc, protocol, timing, and paths; preserve the working local primitive before adapting it.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: digital-forensics
|
|
3
|
+
description: Evidence-preserving workflow for disk, filesystem, memory, archive, document, image, log, browser, and metadata forensics. Use when the task asks what happened, who acted, when an event occurred, or to recover deleted/hidden content from supplied artifacts; use packet-analysis for capture-centric evidence.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# digital forensics
|
|
7
|
+
|
|
8
|
+
preserve provenance while reducing a large artifact set to evidence relevant to the question.
|
|
9
|
+
|
|
10
|
+
1. inventory supplied artifacts, container/archive layers, sizes, timestamps, types, and hashes. work from copies or extracted views when an operation may alter metadata.
|
|
11
|
+
2. translate the user question into evidence categories and a timeline: identities, execution, persistence, access, deletion, transfer, or hidden content.
|
|
12
|
+
3. inspect high-signal metadata and indexes before broad carving or string searches. correlate independent sources rather than trusting one timestamp or parser.
|
|
13
|
+
4. recover embedded, deleted, encoded, or renamed content with format-aware tools. verify recovered object types and relationships to the parent artifact.
|
|
14
|
+
5. keep observed timestamps, normalized time zones, inferred ordering, and uncertain clock assumptions separate.
|
|
15
|
+
|
|
16
|
+
## recovery
|
|
17
|
+
|
|
18
|
+
- parser failure: verify file signatures, truncation, compression, encryption, and tool support; use an alternate parser or inspect the structure directly.
|
|
19
|
+
- no result from a keyword search: broaden through metadata, encodings, alternate names, and timeline neighbors rather than repeating the same strings command.
|
|
20
|
+
- conflicting timestamps: identify each timestamp's semantics and corroborate with logs or adjacent events.
|
|
21
|
+
|
|
22
|
+
## completion
|
|
23
|
+
|
|
24
|
+
answer the forensic question with a traceable artifact or correlation. absence from one parser or index is not proof that evidence never existed.
|
|
@@ -3,11 +3,11 @@ name: ffuf
|
|
|
3
3
|
description: Wordlist strategy, soft-404 filtering, and failure recovery for directory/file fuzzing via the dir_enum tool (ffuf-backed). Use this whenever the user wants to enumerate hidden directories/files/endpoints on a web target, or mentions ffuf, gobuster, or fuzzing.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# directory enumeration
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
use `dir_enum` for the common URL + wordlist case. use `shell_exec` with `ffuf` only when advanced matchers, filters, recursion, headers, methods, or multiple fuzz positions are required.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
useful defaults:
|
|
11
11
|
- Filter noisy 200s from a catch-all page: match by size/words rather than status when the
|
|
12
12
|
target returns 200 for everything (soft-404) — compare a known-bad path's response size first.
|
|
13
13
|
- Start with a small, common wordlist (common.txt / raft-small) before escalating to a large one —
|
|
@@ -16,7 +16,7 @@ Useful defaults:
|
|
|
16
16
|
rather than fuzzing extensions blindly from the start.
|
|
17
17
|
- Recurse only into directories that returned a real (not soft-404) response.
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## failure recovery
|
|
20
20
|
- All-200 responses with identical body size → soft-404 page; filter by size (`-fs <bytes>`) or
|
|
21
21
|
by matching a known regex instead of status code.
|
|
22
22
|
- No hits at all → verify the FUZZ position/URL is actually correct with one manual request first,
|
|
@@ -3,11 +3,11 @@ name: nmap
|
|
|
3
3
|
description: Nmap CLI syntax, safe two-pass scan patterns, and failure recovery (filtered ports, timeouts) for the nmap_scan tool. Use this whenever the user asks to port-scan, enumerate services, or check what's open on a target, even before they mention nmap by name.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# nmap
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
use `port_scan` for the standard `-Pn -sV -sC` scan. use `shell_exec` with `nmap` when the task needs explicit ports, scan types, scripts, timing, UDP, or a deliberate two-pass workflow.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
high-signal flags:
|
|
11
11
|
- `-n` skip DNS resolution
|
|
12
12
|
- `-Pn` skip host discovery when ICMP/ping is filtered (common in lab/VPN targets)
|
|
13
13
|
- `-sS` SYN scan (needs privilege); `-sT` TCP connect scan (no raw-socket privilege needed)
|
|
@@ -18,11 +18,11 @@ High-signal flags:
|
|
|
18
18
|
- `-T4` reasonable speed for lab targets
|
|
19
19
|
- `--max-retries 1 --host-timeout 90s` bound worst-case runtime
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## two-pass workflow
|
|
22
22
|
1. Fast discovery pass: `nmap -n -Pn --top-ports 100 --open -T4 --max-retries 1 --host-timeout 90s <target>`
|
|
23
23
|
2. Enrichment pass on discovered ports only: `nmap -n -Pn -sV -sC -p <comma_ports> --script-timeout 30s --host-timeout 3m <target>`
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## failure recovery
|
|
26
26
|
- Host looks down unexpectedly → add `-Pn` (many lab/CTF targets block ICMP).
|
|
27
27
|
- Scan stalls or times out → tighten `-p`/`--top-ports` and lower `--max-retries`.
|
|
28
28
|
- A "filtered" result that you expect to be open may be transient (target-side rate limiting from
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: packet-analysis
|
|
3
|
+
description: Workflow for PCAP, PCAPNG, network-forensics, protocol-reassembly, malware traffic, and encrypted C2 challenges. Use when packet captures or recorded network streams are primary evidence; combine with binary-reversing when an extracted executable defines encoding or encryption.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# packet analysis
|
|
7
|
+
|
|
8
|
+
turn the capture into a timeline of conversations, artifacts, and transformations.
|
|
9
|
+
|
|
10
|
+
1. preserve the original capture and record its basic properties. inventory endpoints, protocols, ports, conversations, packet counts, durations, and obvious anomalies before applying narrow filters.
|
|
11
|
+
2. build a timeline around the objective. identify which stream contains setup, authentication, delivery, command/control, exfiltration, or the final answer.
|
|
12
|
+
3. reassemble application streams and extract transferred objects with protocol-aware tools when possible. verify extracted file type and hash before analysis.
|
|
13
|
+
4. distinguish capture bytes from dissector interpretation. inspect raw stream bytes when framing, retransmission, encoding, or a custom protocol makes decoded fields misleading.
|
|
14
|
+
5. if content is encoded or encrypted, locate keys and transformations in associated binaries, scripts, configuration, handshakes, or repeated structure; then implement a reproducible decoder.
|
|
15
|
+
|
|
16
|
+
## recovery
|
|
17
|
+
|
|
18
|
+
- no useful high-level decode: follow individual streams, inspect raw payloads, and infer framing from direction and length.
|
|
19
|
+
- apparent missing data: check packet loss, truncation, out-of-order segments, retransmissions, alternate channels, and archive contents before concluding absence.
|
|
20
|
+
- too much traffic: rank conversations by timing, volume, protocol, and relationship to the known event instead of applying random display filters.
|
|
21
|
+
|
|
22
|
+
## completion
|
|
23
|
+
|
|
24
|
+
support the answer with the relevant stream, extracted artifact, timeline, or decoder output. protocol labels and suspicious traffic alone are not the conclusion.
|