farai 0.2.5 → 0.2.6

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.
@@ -27,13 +27,13 @@ FROM kalilinux/kali-rolling
27
27
  ENV DEBIAN_FRONTEND=noninteractive
28
28
 
29
29
  ARG TARGETARCH
30
- ARG NAABU_VERSION=2.6.1
31
30
  ARG INTERACTSH_VERSION=1.3.1
32
31
  ARG TYPESCRIPT_VERSION=5.9.3
33
32
  ARG TYPESCRIPT_LANGUAGE_SERVER_VERSION=4.3.4
34
33
  ARG PYRIGHT_VERSION=1.1.408
34
+ ARG KALI_CONTRACT=curated-security-runtime-2026-09
35
35
 
36
- LABEL org.farai.kali.contract="curated-security-runtime"
36
+ LABEL org.farai.kali.contract="${KALI_CONTRACT}"
37
37
 
38
38
  COPY farai-tool-manifest.json /usr/local/share/farai/farai-tool-manifest.json
39
39
 
@@ -46,28 +46,45 @@ RUN apt-get update \
46
46
  RUN set -eux; \
47
47
  case "${TARGETARCH:-amd64}" in \
48
48
  amd64) \
49
- naabu_arch=amd64; naabu_sha=018c4c9884dea971eda860435ede3021d1150732f34cfd245498c6726d8cab90; \
49
+ pd_arch=amd64; \
50
50
  interactsh_arch=amd64; interactsh_sha=d553fb3dd4c2684c953aedeb555f13f0d1ca94bd75112c949a5fd975d385a903 ;; \
51
51
  arm64) \
52
- naabu_arch=arm64; naabu_sha=3adc2bb2395c3efff89623499b20eea66ef54924c485d3ae86762393a31736ea; \
52
+ pd_arch=arm64; \
53
53
  interactsh_arch=arm64; interactsh_sha=475aa78c59afad115149a5b0d6d9c99d2410a4f12d0c6f5a24c32e7338fc6adb ;; \
54
54
  *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \
55
55
  esac; \
56
- curl -fsSL "https://github.com/projectdiscovery/naabu/releases/download/v${NAABU_VERSION}/naabu_${NAABU_VERSION}_linux_${naabu_arch}.zip" -o /tmp/naabu.zip; \
57
- echo "${naabu_sha} /tmp/naabu.zip" | sha256sum -c -; \
58
- unzip -q /tmp/naabu.zip -d /tmp/naabu; \
59
- install -m 0755 /tmp/naabu/naabu /usr/local/bin/naabu; \
56
+ install_pd() { \
57
+ tool="$1"; \
58
+ version="$(python3 -c 'import json,sys; print(json.load(open("/usr/local/share/farai/farai-tool-manifest.json", encoding="utf-8"))["pinnedTools"][sys.argv[1]]["version"])' "${tool}")"; \
59
+ checksum="$(python3 -c 'import json,sys; print(json.load(open("/usr/local/share/farai/farai-tool-manifest.json", encoding="utf-8"))["pinnedTools"][sys.argv[1]]["sha256"][sys.argv[2]])' "${tool}" "${pd_arch}")"; \
60
+ archive="/tmp/${tool}.zip"; \
61
+ directory="/tmp/${tool}"; \
62
+ curl -fsSL "https://github.com/projectdiscovery/${tool}/releases/download/v${version}/${tool}_${version}_linux_${pd_arch}.zip" -o "${archive}"; \
63
+ echo "${checksum} ${archive}" | sha256sum -c -; \
64
+ mkdir -p "${directory}"; \
65
+ unzip -q "${archive}" -d "${directory}"; \
66
+ install -m 0755 "${directory}/${tool}" "/usr/local/bin/${tool}"; \
67
+ }; \
68
+ for tool in dnsx httpx katana naabu nuclei subfinder tlsx urlfinder vulnx; do install_pd "${tool}"; done; \
60
69
  curl -fsSL "https://github.com/projectdiscovery/interactsh/releases/download/v${INTERACTSH_VERSION}/interactsh-client_${INTERACTSH_VERSION}_linux_${interactsh_arch}.zip" -o /tmp/interactsh.zip; \
61
70
  echo "${interactsh_sha} /tmp/interactsh.zip" | sha256sum -c -; \
62
71
  unzip -q /tmp/interactsh.zip -d /tmp/interactsh; \
63
72
  install -m 0755 /tmp/interactsh/interactsh-client /usr/local/bin/interactsh-client; \
64
- rm -rf /tmp/naabu /tmp/interactsh /tmp/naabu.zip /tmp/interactsh.zip
73
+ templates_version="$(python3 -c 'import json; print(json.load(open("/usr/local/share/farai/farai-tool-manifest.json", encoding="utf-8"))["pinnedAssets"]["nuclei-templates"]["version"])')"; \
74
+ templates_checksum="$(python3 -c 'import json; print(json.load(open("/usr/local/share/farai/farai-tool-manifest.json", encoding="utf-8"))["pinnedAssets"]["nuclei-templates"]["sha256"])')"; \
75
+ curl -fsSL "https://github.com/projectdiscovery/nuclei-templates/archive/refs/tags/v${templates_version}.zip" -o /tmp/nuclei-templates.zip; \
76
+ echo "${templates_checksum} /tmp/nuclei-templates.zip" | sha256sum -c -; \
77
+ unzip -q /tmp/nuclei-templates.zip -d /tmp/nuclei-templates; \
78
+ mkdir -p /usr/local/share/farai/nuclei-templates; \
79
+ cp -a "/tmp/nuclei-templates/nuclei-templates-${templates_version}/." /usr/local/share/farai/nuclei-templates/; \
80
+ rm -rf /tmp/dnsx /tmp/httpx /tmp/katana /tmp/naabu /tmp/nuclei /tmp/subfinder /tmp/tlsx /tmp/urlfinder /tmp/vulnx /tmp/interactsh /tmp/nuclei-templates \
81
+ /tmp/dnsx.zip /tmp/httpx.zip /tmp/katana.zip /tmp/naabu.zip /tmp/nuclei.zip /tmp/subfinder.zip /tmp/tlsx.zip /tmp/urlfinder.zip /tmp/vulnx.zip /tmp/interactsh.zip /tmp/nuclei-templates.zip
65
82
 
66
83
  RUN curl -LsSf https://astral.sh/uv/install.sh | sh \
67
84
  && ln -sf /root/.local/bin/uv /usr/local/bin/uv \
68
85
  && ln -sf /root/.local/bin/uvx /usr/local/bin/uvx
69
86
 
70
- RUN UV_TOOL_DIR=/opt/uv/tools UV_TOOL_BIN_DIR=/opt/uv/bin uv tool install --with fastmcp mitmproxy-mcp==0.6.1 \
87
+ RUN UV_TOOL_DIR=/opt/uv/tools UV_TOOL_BIN_DIR=/opt/uv/bin uv tool install --with 'mcp>=1.25.0,<2' mitmproxy-mcp==0.6.1 \
71
88
  && chmod -R a+rX /opt/uv \
72
89
  && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install -g \
73
90
  @playwright/mcp@latest \
@@ -24,6 +24,8 @@ MANIFEST = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
24
24
  CONTRACT = MANIFEST["contract"]
25
25
  APT_PACKAGES = tuple(MANIFEST["aptPackages"])
26
26
  WORKFLOWS = MANIFEST["workflows"]
27
+ PINNED_TOOLS = MANIFEST["pinnedTools"]
28
+ PINNED_ASSETS = MANIFEST["pinnedAssets"]
27
29
  REQUIRED = tuple(dict.fromkeys(command for commands in WORKFLOWS.values() for command in commands))
28
30
 
29
31
 
@@ -61,6 +63,7 @@ def missing_packages():
61
63
  commands = installed_commands()
62
64
  missing = [name for name in REQUIRED if name not in commands]
63
65
  missing_apt_packages = missing_packages()
66
+ missing_assets = [name for name, asset in PINNED_ASSETS.items() if not Path(asset["path"]).exists()]
64
67
  payload = {
65
68
  "contract": CONTRACT,
66
69
  "manifest": str(MANIFEST_PATH),
@@ -69,6 +72,9 @@ payload = {
69
72
  "requiredCount": len(REQUIRED),
70
73
  "missing": missing,
71
74
  "missingPackages": missing_apt_packages,
75
+ "missingAssets": missing_assets,
76
+ "pinnedTools": PINNED_TOOLS,
77
+ "pinnedAssets": PINNED_ASSETS,
72
78
  "workflows": WORKFLOWS,
73
79
  }
74
80
 
@@ -80,4 +86,4 @@ if len(sys.argv) == 3 and sys.argv[1] == "--write":
80
86
  else:
81
87
  print(json.dumps(payload, ensure_ascii=True, separators=(",", ":")))
82
88
 
83
- raise SystemExit(0 if not missing and not missing_apt_packages else 1)
89
+ raise SystemExit(0 if not missing and not missing_apt_packages and not missing_assets else 1)
@@ -1,5 +1,5 @@
1
1
  {
2
- "contract": "curated-security-runtime",
2
+ "contract": "curated-security-runtime-2026-09",
3
3
  "aptPackages": [
4
4
  "amass",
5
5
  "arjun",
@@ -16,7 +16,6 @@
16
16
  "dirsearch",
17
17
  "dnsenum",
18
18
  "dnsrecon",
19
- "dnsx",
20
19
  "enum4linux-ng",
21
20
  "evil-winrm",
22
21
  "exploitdb",
@@ -54,7 +53,6 @@
54
53
  "nmap",
55
54
  "nodejs",
56
55
  "npm",
57
- "nuclei",
58
56
  "openssh-client",
59
57
  "openssl",
60
58
  "poppler-utils",
@@ -75,7 +73,6 @@
75
73
  "sslscan",
76
74
  "sslyze",
77
75
  "steghide",
78
- "subfinder",
79
76
  "tcpdump",
80
77
  "tesseract-ocr",
81
78
  "testssl.sh",
@@ -96,6 +93,78 @@
96
93
  "yara",
97
94
  "zip"
98
95
  ],
96
+ "pinnedTools": {
97
+ "dnsx": {
98
+ "version": "1.3.1",
99
+ "sha256": {
100
+ "amd64": "438b964653056dd51dcfe614b1a16f8bced3cc48a1d27bc07cc6fdf2ef2a9533",
101
+ "arm64": "dd657dd1ccee5e137eca2dbad0e97dbd067555f744adb97efcda774f1b2fbde1"
102
+ }
103
+ },
104
+ "httpx": {
105
+ "version": "1.11.0",
106
+ "sha256": {
107
+ "amd64": "5dce96e7315cff24be7aab3b032f23f7a71b7711a17f8189ebe601f2588f2f87",
108
+ "arm64": "2b5d9ec3d3750df41e04e4216b1400d2ad862d1cb9ebe68ae501b2299cb5b3a5"
109
+ }
110
+ },
111
+ "katana": {
112
+ "version": "1.7.0",
113
+ "sha256": {
114
+ "amd64": "fe1142d92f418549338ea46d67a472124878482e225d279e9a42700c75d76a4d",
115
+ "arm64": "9a6885fe9fda850129b110e0f079d42529b0693dd81b59c316f04084935300f0"
116
+ }
117
+ },
118
+ "naabu": {
119
+ "version": "2.6.1",
120
+ "sha256": {
121
+ "amd64": "018c4c9884dea971eda860435ede3021d1150732f34cfd245498c6726d8cab90",
122
+ "arm64": "3adc2bb2395c3efff89623499b20eea66ef54924c485d3ae86762393a31736ea"
123
+ }
124
+ },
125
+ "nuclei": {
126
+ "version": "3.11.1",
127
+ "sha256": {
128
+ "amd64": "ea63d4ae232808cd7c6bc00d0142428e231fab59dae01042246097d195835ab6",
129
+ "arm64": "8044e3d9768ba0a744b2872c1a87e813006f013da97ca9f50f7661a4203bec07"
130
+ }
131
+ },
132
+ "subfinder": {
133
+ "version": "2.16.0",
134
+ "sha256": {
135
+ "amd64": "1b7f9c608e9a5bd59e609a5e09710d63c5485e92d3d49dc2c16eb4fdbe10cb60",
136
+ "arm64": "c81d49559c0f630177be9e347e502e7a3d474aacc6ff78291ffcb4964367d63d"
137
+ }
138
+ },
139
+ "tlsx": {
140
+ "version": "1.3.0",
141
+ "sha256": {
142
+ "amd64": "d982c55da676d7f6bfe588550ed19946e3c73234b4a3a11caf105f3ab98564bf",
143
+ "arm64": "70a65a286eaeb336d04c06fd1783541096f7b06c5c2e7a22467406ea2c323449"
144
+ }
145
+ },
146
+ "urlfinder": {
147
+ "version": "0.0.3",
148
+ "sha256": {
149
+ "amd64": "655ea7378a0555149831dcc4c3f6aef34f3ce27eda0ce2f2721e2ca5526015d4",
150
+ "arm64": "6af33108d6b76bbd261fa696c69dccf44b9eb5a68dc47b4c41df5cf32f8b8281"
151
+ }
152
+ },
153
+ "vulnx": {
154
+ "version": "2.0.2",
155
+ "sha256": {
156
+ "amd64": "387abd1c42b0357e1d4257b54528fc38e1d0357b12f05ab12117a4c628a5aaf8",
157
+ "arm64": "13c5888dde027b84bd3b6cf69c36c87e1e7aad7e7159bdeeb576f4962dcc1d06"
158
+ }
159
+ }
160
+ },
161
+ "pinnedAssets": {
162
+ "nuclei-templates": {
163
+ "version": "10.4.8",
164
+ "path": "/usr/local/share/farai/nuclei-templates",
165
+ "sha256": "cb4f88504ebf9d462c2db4c8d2bdf6cb94ecc7b7f4ed09b7f4fd20ceb6bc8002"
166
+ }
167
+ },
99
168
  "workflows": {
100
169
  "shell-and-network-utilities": [
101
170
  "curl",
@@ -136,9 +205,11 @@
136
205
  "theHarvester"
137
206
  ],
138
207
  "service-discovery": [
208
+ "httpx",
139
209
  "masscan",
140
210
  "naabu",
141
- "nmap"
211
+ "nmap",
212
+ "tlsx"
142
213
  ],
143
214
  "callback-verification": [
144
215
  "interactsh-client",
@@ -150,7 +221,9 @@
150
221
  "dirsearch",
151
222
  "feroxbuster",
152
223
  "ffuf",
153
- "gobuster"
224
+ "gobuster",
225
+ "katana",
226
+ "urlfinder"
154
227
  ],
155
228
  "web-assessment": [
156
229
  "commix",
@@ -166,6 +239,9 @@
166
239
  "whatweb",
167
240
  "wpscan"
168
241
  ],
242
+ "vulnerability-intelligence": [
243
+ "vulnx"
244
+ ],
169
245
  "credential-access": [
170
246
  "hashcat",
171
247
  "hydra",
@@ -17,6 +17,7 @@ from mitmproxy_mcp.core import server
17
17
 
18
18
 
19
19
  UPSTREAM_TRAFFIC_RECORDER = server.TrafficRecorder
20
+ UPSTREAM_MITM_CONTROLLER = server.MitmController
20
21
 
21
22
 
22
23
  def env_int(name: str, default: int, minimum: int, maximum: int) -> int:
@@ -33,6 +34,84 @@ MAX_MESSAGES = env_int("FARAI_PROXY_MAX_MESSAGES", 200, 1, 1000)
33
34
  REPLAY_CORRELATION_HEADER = "X-Farai-Replay-Correlation"
34
35
 
35
36
 
37
+ def env_json_strings(name: str) -> list[str]:
38
+ try:
39
+ value = json.loads(os.environ.get(name, "[]"))
40
+ except json.JSONDecodeError:
41
+ return []
42
+ if not isinstance(value, list):
43
+ return []
44
+ return normalize_pass_through_hosts(value)
45
+
46
+
47
+ def normalize_pass_through_hosts(values: Any) -> list[str]:
48
+ if not isinstance(values, list):
49
+ raise ValueError("pass_through_hosts must be a list")
50
+ hosts: list[str] = []
51
+ for value in values:
52
+ if not isinstance(value, str):
53
+ raise ValueError("pass_through_hosts must contain strings")
54
+ host = value.strip().lower()
55
+ if not host or any(char in host for char in "\r\n\0"):
56
+ raise ValueError("pass_through_hosts contains an invalid host")
57
+ if host not in hosts:
58
+ hosts.append(host)
59
+ return hosts
60
+
61
+
62
+ def pass_through_pattern(host: str) -> str:
63
+ if host.startswith("*."):
64
+ return rf"^(?:[^:]+\.)?{re.escape(host[2:])}(?::\d+)?$"
65
+ return rf"^{re.escape(host)}(?::\d+)?$"
66
+
67
+
68
+ class FaraiMitmController(UPSTREAM_MITM_CONTROLLER):
69
+ def __init__(self, dump_file: str | None = None):
70
+ super().__init__(dump_file=dump_file)
71
+ configured_tls = os.environ.get("FARAI_PROXY_TLS", "relaxed").strip().lower()
72
+ self.farai_tls_mode = configured_tls if configured_tls in {"strict", "relaxed"} else "relaxed"
73
+ self.farai_pass_through_hosts = env_json_strings("FARAI_PROXY_PASS_THROUGH_HOSTS")
74
+
75
+ async def start(
76
+ self,
77
+ port: int = 8080,
78
+ host: str = "127.0.0.1",
79
+ dump_file: str | None = None,
80
+ ):
81
+ result = await super().start(port=port, host=host, dump_file=dump_file)
82
+ self.apply_farai_policy()
83
+ return result
84
+
85
+ def apply_farai_policy(self) -> None:
86
+ if self.master is None:
87
+ return
88
+ self.master.options.update(
89
+ ssl_insecure=self.farai_tls_mode == "relaxed",
90
+ ignore_hosts=[pass_through_pattern(host) for host in self.farai_pass_through_hosts],
91
+ )
92
+
93
+ def farai_policy_state(self) -> dict[str, Any]:
94
+ return {
95
+ "tls": self.farai_tls_mode,
96
+ "passThroughHosts": list(self.farai_pass_through_hosts),
97
+ }
98
+
99
+ def set_farai_policy(
100
+ self,
101
+ tls_mode: str | None = None,
102
+ pass_through_hosts: list[str] | None = None,
103
+ ) -> dict[str, Any]:
104
+ if tls_mode is not None:
105
+ normalized_tls = tls_mode.strip().lower()
106
+ if normalized_tls not in {"strict", "relaxed"}:
107
+ raise ValueError("tls_mode must be strict or relaxed")
108
+ self.farai_tls_mode = normalized_tls
109
+ if pass_through_hosts is not None:
110
+ self.farai_pass_through_hosts = normalize_pass_through_hosts(pass_through_hosts)
111
+ self.apply_farai_policy()
112
+ return self.farai_policy_state()
113
+
114
+
36
115
  class FlowStore:
37
116
  def __init__(self, db_path: str):
38
117
  self.db_path = db_path
@@ -138,6 +217,13 @@ class FaraiTrafficRecorder(UPSTREAM_TRAFFIC_RECORDER):
138
217
  self._capture(flow)
139
218
 
140
219
  def error(self, flow: http.HTTPFlow) -> None:
220
+ message = flow_error(flow)
221
+ if flow.response is None and message and upstream_tls_verification_failed(message):
222
+ flow.response = http.Response.make(
223
+ 502,
224
+ b"farai proxy: upstream tls verification failed; use relaxed tls or pass-through for this host\n",
225
+ {"Content-Type": "text/plain; charset=utf-8", "X-Farai-Proxy-Error": "upstream-tls-verification-failed"},
226
+ )
141
227
  super().error(flow)
142
228
  self._capture(flow)
143
229
 
@@ -624,6 +710,11 @@ def flow_error(flow: Any) -> str | None:
624
710
  return text_value(getattr(error, "msg", error))
625
711
 
626
712
 
713
+ def upstream_tls_verification_failed(message: str) -> bool:
714
+ normalized = message.lower()
715
+ return "certificate verify failed" in normalized or "certificate verification failed" in normalized
716
+
717
+
627
718
  def text_value(value: Any, encoding: str = "utf-8") -> str:
628
719
  if isinstance(value, bytes):
629
720
  return value.decode(encoding, errors="replace")
@@ -720,6 +811,28 @@ async def proxy_scope_get() -> str:
720
811
  return json.dumps(recorder.scope_state(), indent=2)
721
812
 
722
813
 
814
+ @server.mcp.tool()
815
+ async def proxy_policy_get() -> str:
816
+ controller = server.controller
817
+ if not isinstance(controller, FaraiMitmController):
818
+ return json.dumps({"tls": "strict", "passThroughHosts": []}, indent=2)
819
+ return json.dumps(controller.farai_policy_state(), indent=2)
820
+
821
+
822
+ @server.mcp.tool()
823
+ async def proxy_policy_set(
824
+ tls_mode: str | None = None,
825
+ pass_through_hosts: list[str] | None = None,
826
+ ) -> str:
827
+ controller = server.controller
828
+ if not isinstance(controller, FaraiMitmController):
829
+ raise RuntimeError("Farai proxy policy controller is unavailable.")
830
+ return json.dumps(
831
+ controller.set_farai_policy(tls_mode, pass_through_hosts),
832
+ indent=2,
833
+ )
834
+
835
+
723
836
  @server.mcp.tool()
724
837
  async def proxy_intercept_configure(
725
838
  enabled: bool,
@@ -787,6 +900,7 @@ async def proxy_intercept_resolve(
787
900
 
788
901
 
789
902
  server.TrafficRecorder = FaraiTrafficRecorder
903
+ server.MitmController = FaraiMitmController
790
904
 
791
905
 
792
906
  if __name__ == "__main__":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "farai",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Cyber-first freestyle local AI agent",
@@ -46,7 +46,7 @@
46
46
  "tui:test": "bun test ./test/tui-*.test.ts ./test/tui-*.test.tsx",
47
47
  "doctor": "bun run src/cli/index.ts doctor",
48
48
  "typecheck": "tsc --noEmit",
49
- "test": "bun test ./test",
49
+ "test": "bun test ./test/*.test.ts ./test/*.test.tsx",
50
50
  "verify": "bun run typecheck && bun run test && bun run test:package",
51
51
  "release:check": "bun run verify && bun audit",
52
52
  "eval:smoke": "bun run test/evals/smoke.ts",