kifaru 1.0.201 → 1.0.202

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.
Files changed (2) hide show
  1. package/kifaru-pi.js +136 -122
  2. package/package.json +2 -2
package/kifaru-pi.js CHANGED
@@ -51515,52 +51515,27 @@ ${result.stderr}`.trim().split(/\r?\n/).find((line) => line.trim().length > 0)?.
51515
51515
  })(PythonRuntime ||= {});
51516
51516
  });
51517
51517
 
51518
- // ../execution-runtime/src/python-helpers.ts
51519
- import { createHash as createHash4 } from "crypto";
51520
- import fs9 from "fs/promises";
51521
- import path12 from "path";
51522
- var PythonHelpers;
51523
- var init_python_helpers = __esm(() => {
51524
- init_python();
51525
- init_paths();
51526
- ((PythonHelpers) => {
51527
- PythonHelpers.HUNT_MODULE_SOURCE = `"""kifaru hunt helpers - stdlib-only HTTP / crypto / recon utilities.
51518
+ // ../execution-runtime/src/hunt-modules.ts
51519
+ var HUNT_MODULE_FILES;
51520
+ var init_hunt_modules = __esm(() => {
51521
+ HUNT_MODULE_FILES = {
51522
+ "hunt.py": `"""Kifaru's stdlib-only hunt helpers.
51528
51523
 
51529
- The API mirrors requests, so your usual idioms work unchanged:
51530
51524
  import hunt
51531
- r = hunt.post("https://target/api/login", json={"user": "a", "pass": "b"})
51532
- print(r.status_code, r.text) # .status / .body also work
51533
- tok = r.json()["token"]
51534
- me = hunt.get("https://target/api/me", token=tok)
51535
- print(me.status_code, me.ok, hunt.jwt_decode(tok)["payload"])
51536
-
51537
- Response fields: .status_code / .status, .text / .body, .content, .json(), .ok, .headers.
51538
- Request kwargs: json= (JSON body, requests-style), body= (alias), data= (form/raw),
51539
- token= (Authorization), headers=, timeout=, allow_redirects=.
51540
-
51541
- Or use it as a CLI:
51542
- python -m hunt GET https://target/api -H "Authorization: Bearer ey..."
51543
-
51544
- No third-party packages required and no need to match a custom API - it behaves like
51545
- requests. When the managed venv is active you also have requests/httpx/PyJWT/dnspython,
51546
- but everything here works on the standard library alone; prefer it over urllib boilerplate.
51525
+ r = hunt.post("https://target/api/login", json={"user": "a"})
51526
+ print(r.status_code, r.text, r.json())
51527
+
51528
+ Request kwargs: json=, body=, data=, token=, headers=, timeout=,
51529
+ allow_redirects=. Response fields: .status_code/.status, .text/.body,
51530
+ .content, .json(), .ok, .headers.
51547
51531
  """
51548
51532
  from __future__ import annotations
51549
51533
 
51550
- import base64
51551
- import hashlib
51552
- import hmac
51553
- import json as _json
51554
- import os
51555
- import random
51556
- import socket
51557
- import ssl
51558
- import string
51559
51534
  import sys
51560
- import time
51561
- import urllib.error
51562
- import urllib.parse
51563
- import urllib.request
51535
+
51536
+ from hunt_http import DEFAULT_UA, Resp, get, post, req
51537
+ from hunt_crypto import b64d, b64e, jwt_decode, sign
51538
+ from hunt_recon import now_ms, rand, rand_email, resolve
51564
51539
 
51565
51540
  __all__ = [
51566
51541
  "req", "get", "post", "Resp",
@@ -51568,10 +51543,58 @@ __all__ = [
51568
51543
  "rand", "rand_email", "resolve", "sign", "now_ms",
51569
51544
  ]
51570
51545
 
51546
+
51547
+ def _cli(argv):
51548
+ args = list(argv)
51549
+ if len(args) < 2 or args[0] in ("-h", "--help"):
51550
+ print("usage: python -m hunt METHOD URL [-H 'K: V']... [-d BODY] [-t TOKEN] [--no-redirect]")
51551
+ return 0 if not args or args[0] in ("-h", "--help") else 2
51552
+ method, url = args[0], args[1]
51553
+ headers, body, token, redirects = {}, None, None, True
51554
+ i = 2
51555
+ while i < len(args):
51556
+ arg = args[i]
51557
+ if arg in ("-H", "--header") and i + 1 < len(args):
51558
+ key, _, value = args[i + 1].partition(":")
51559
+ headers[key.strip()] = value.strip()
51560
+ i += 2
51561
+ elif arg in ("-d", "--data") and i + 1 < len(args):
51562
+ body = args[i + 1]
51563
+ i += 2
51564
+ elif arg in ("-t", "--token") and i + 1 < len(args):
51565
+ token = args[i + 1]
51566
+ i += 2
51567
+ elif arg == "--no-redirect":
51568
+ redirects = False
51569
+ i += 1
51570
+ else:
51571
+ i += 1
51572
+ response = req(method, url, data=body, token=token, headers=headers or None,
51573
+ allow_redirects=redirects)
51574
+ print("HTTP %s %s" % (response.status, response.url))
51575
+ for key, value in response.headers.items():
51576
+ print("%s: %s" % (key, value))
51577
+ print()
51578
+ print(response.error if response.error else response.body)
51579
+ return 0 if response.ok else 1
51580
+
51581
+
51582
+ if __name__ == "__main__":
51583
+ raise SystemExit(_cli(sys.argv[1:]))
51584
+ `,
51585
+ "hunt_http.py": `"""Small requests-like HTTP surface for hunt."""
51586
+ from __future__ import annotations
51587
+
51588
+ import json as _json
51589
+ import os
51590
+ import ssl
51591
+ import urllib.error
51592
+ import urllib.parse
51593
+ import urllib.request
51594
+
51571
51595
  _CTX = ssl.create_default_context()
51572
51596
  _CTX.check_hostname = False
51573
51597
  _CTX.verify_mode = ssl.CERT_NONE
51574
-
51575
51598
  DEFAULT_UA = os.environ.get("HUNT_UA", "kifaru-research")
51576
51599
 
51577
51600
 
@@ -51596,7 +51619,6 @@ class Resp:
51596
51619
  except Exception:
51597
51620
  return default
51598
51621
 
51599
- # requests/httpx-compatible aliases so existing muscle memory just works.
51600
51622
  @property
51601
51623
  def status_code(self):
51602
51624
  return self.status
@@ -51622,16 +51644,9 @@ class Resp:
51622
51644
 
51623
51645
  def req(method, url, body=None, json=None, data=None, token=None, headers=None,
51624
51646
  read=200000, timeout=20, allow_redirects=True):
51625
- """One HTTP request via urllib. Never raises on HTTP status.
51626
-
51627
- body / json -> JSON-encoded payload (json= is the requests-style alias);
51628
- data -> dict(form) / str / bytes.
51629
- token -> Authorization header (adds 'Bearer ' if no scheme given).
51630
- Returns Resp with requests-compatible fields: .status_code/.status,
51631
- .text/.body, .json(), .ok, .headers.
51632
- """
51647
+ """One HTTP request. HTTP status errors return Resp rather than raising."""
51633
51648
  if body is None:
51634
- body = json # accept requests-style json= kwarg
51649
+ body = json
51635
51650
  h = {"User-Agent": DEFAULT_UA}
51636
51651
  payload = None
51637
51652
  if body is not None:
@@ -51656,13 +51671,14 @@ def req(method, url, body=None, json=None, data=None, token=None, headers=None,
51656
51671
  handlers.append(_NoRedirect())
51657
51672
  opener = urllib.request.build_opener(*handlers)
51658
51673
  try:
51659
- with opener.open(request, timeout=timeout) as resp:
51660
- return Resp(resp.status, dict(resp.headers),
51661
- resp.read(read).decode("utf-8", "ignore"), resp.geturl())
51662
- except urllib.error.HTTPError as e:
51663
- return Resp(e.code, dict(e.headers), e.read(read).decode("utf-8", "ignore"), url)
51664
- except Exception as e:
51665
- return Resp(0, {}, "", url, error="%s: %s" % (type(e).__name__, e))
51674
+ with opener.open(request, timeout=timeout) as response:
51675
+ return Resp(response.status, dict(response.headers),
51676
+ response.read(read).decode("utf-8", "ignore"), response.geturl())
51677
+ except urllib.error.HTTPError as error:
51678
+ return Resp(error.code, dict(error.headers),
51679
+ error.read(read).decode("utf-8", "ignore"), url)
51680
+ except Exception as error:
51681
+ return Resp(0, {}, "", url, error="%s: %s" % (type(error).__name__, error))
51666
51682
 
51667
51683
 
51668
51684
  def get(url, **kw):
@@ -51671,30 +51687,38 @@ def get(url, **kw):
51671
51687
 
51672
51688
  def post(url, body=None, **kw):
51673
51689
  return req("POST", url, body=body, **kw)
51690
+ `,
51691
+ "hunt_crypto.py": `"""Encoding, JWT inspection, and HMAC helpers for hunt."""
51692
+ from __future__ import annotations
51693
+
51694
+ import base64
51695
+ import hashlib
51696
+ import hmac
51697
+ import json as _json
51674
51698
 
51675
51699
 
51676
- def b64d(s, urlsafe=True):
51677
- s = s if isinstance(s, str) else s.decode()
51678
- s += "=" * (-len(s) % 4)
51679
- return (base64.urlsafe_b64decode if urlsafe else base64.b64decode)(s)
51700
+ def b64d(value, urlsafe=True):
51701
+ value = value if isinstance(value, str) else value.decode()
51702
+ value += "=" * (-len(value) % 4)
51703
+ return (base64.urlsafe_b64decode if urlsafe else base64.b64decode)(value)
51680
51704
 
51681
51705
 
51682
- def b64e(b, urlsafe=True):
51683
- if isinstance(b, str):
51684
- b = b.encode()
51685
- out = (base64.urlsafe_b64encode if urlsafe else base64.b64encode)(b)
51686
- return out.decode().rstrip("=")
51706
+ def b64e(value, urlsafe=True):
51707
+ if isinstance(value, str):
51708
+ value = value.encode()
51709
+ encoded = (base64.urlsafe_b64encode if urlsafe else base64.b64encode)(value)
51710
+ return encoded.decode().rstrip("=")
51687
51711
 
51688
51712
 
51689
51713
  def jwt_decode(token):
51690
- """Decode a JWT without verifying. Returns {"header":..., "payload":..., "signature":str}."""
51714
+ """Decode a JWT without verifying its signature."""
51691
51715
  parts = token.split(".")
51692
51716
  if len(parts) < 2:
51693
51717
  raise ValueError("not a JWT")
51694
51718
  header = _json.loads(b64d(parts[0]))
51695
51719
  payload = _json.loads(b64d(parts[1]))
51696
- sig = parts[2] if len(parts) > 2 else ""
51697
- return {"header": header, "payload": payload, "signature": sig}
51720
+ signature = parts[2] if len(parts) > 2 else ""
51721
+ return {"header": header, "payload": payload, "signature": signature}
51698
51722
 
51699
51723
 
51700
51724
  def sign(key, msg, alg="sha256"):
@@ -51704,14 +51728,22 @@ def sign(key, msg, alg="sha256"):
51704
51728
  if isinstance(msg, str):
51705
51729
  msg = msg.encode()
51706
51730
  return hmac.new(key, msg, getattr(hashlib, alg)).hexdigest()
51731
+ `,
51732
+ "hunt_recon.py": `"""DNS, random test data, and timing helpers for hunt."""
51733
+ from __future__ import annotations
51734
+
51735
+ import random
51736
+ import socket
51737
+ import string
51738
+ import time
51707
51739
 
51708
51740
 
51709
51741
  def resolve(host):
51710
51742
  """All A/AAAA addresses for host (DNS)."""
51711
51743
  try:
51712
- return sorted({ai[4][0] for ai in socket.getaddrinfo(host, None)})
51713
- except Exception as e:
51714
- return ["__error__ %s: %s" % (type(e).__name__, e)]
51744
+ return sorted({address[4][0] for address in socket.getaddrinfo(host, None)})
51745
+ except Exception as error:
51746
+ return ["__error__ %s: %s" % (type(error).__name__, error)]
51715
51747
 
51716
51748
 
51717
51749
  def rand(n=8, alphabet=string.ascii_lowercase + string.digits):
@@ -51724,46 +51756,24 @@ def rand_email(domain="example.com"):
51724
51756
 
51725
51757
  def now_ms():
51726
51758
  return int(time.time() * 1000)
51759
+ `
51760
+ };
51761
+ });
51727
51762
 
51728
-
51729
- def _cli(argv):
51730
- args = list(argv)
51731
- if not args or args[0] in ("-h", "--help"):
51732
- print("usage: python -m hunt METHOD URL [-H 'K: V']... [-d BODY] [-t TOKEN] [--no-redirect]")
51733
- return 0
51734
- method, url = args[0], args[1]
51735
- headers, body, token, redirects = {}, None, None, True
51736
- i = 2
51737
- while i < len(args):
51738
- a = args[i]
51739
- if a in ("-H", "--header") and i + 1 < len(args):
51740
- k, _, v = args[i + 1].partition(":")
51741
- headers[k.strip()] = v.strip()
51742
- i += 2
51743
- elif a in ("-d", "--data") and i + 1 < len(args):
51744
- body = args[i + 1]
51745
- i += 2
51746
- elif a in ("-t", "--token") and i + 1 < len(args):
51747
- token = args[i + 1]
51748
- i += 2
51749
- elif a == "--no-redirect":
51750
- redirects = False
51751
- i += 1
51752
- else:
51753
- i += 1
51754
- r = req(method, url, data=body, token=token, headers=headers or None, allow_redirects=redirects)
51755
- print("HTTP %s %s" % (r.status, r.url))
51756
- for k, v in r.headers.items():
51757
- print("%s: %s" % (k, v))
51758
- print()
51759
- print(r.error if r.error else r.body)
51760
- return 0 if r.ok else 1
51761
-
51762
-
51763
- if __name__ == "__main__":
51764
- raise SystemExit(_cli(sys.argv[1:]))
51765
- `;
51766
- PythonHelpers.HUNT_MODULE_VERSION = createHash4("sha256").update(PythonHelpers.HUNT_MODULE_SOURCE).digest("hex").slice(0, 12);
51763
+ // ../execution-runtime/src/python-helpers.ts
51764
+ import { createHash as createHash4 } from "crypto";
51765
+ import fs9 from "fs/promises";
51766
+ import path12 from "path";
51767
+ var PythonHelpers;
51768
+ var init_python_helpers = __esm(() => {
51769
+ init_hunt_modules();
51770
+ init_python();
51771
+ init_paths();
51772
+ ((PythonHelpers) => {
51773
+ PythonHelpers.HUNT_MODULE_SOURCE = HUNT_MODULE_FILES["hunt.py"];
51774
+ PythonHelpers.HUNT_MODULE_VERSION = createHash4("sha256").update(Object.entries(HUNT_MODULE_FILES).sort(([a], [b]) => a.localeCompare(b)).map(([name21, source]) => `${name21}
51775
+ ${source}`).join(`
51776
+ `)).digest("hex").slice(0, 12);
51767
51777
  function defaultDeps() {
51768
51778
  return {
51769
51779
  mkdir: (dir) => fs9.mkdir(dir, { recursive: true }).then(() => {
@@ -51784,24 +51794,27 @@ if __name__ == "__main__":
51784
51794
  if (cached3)
51785
51795
  return cached3;
51786
51796
  const task = (async () => {
51787
- const target = path12.join(dir, "hunt.py");
51788
- const stamp = path12.join(dir, ".hunt-version");
51789
51797
  try {
51790
- const [onDisk, current] = await Promise.all([
51791
- deps2.readFile(target).catch(() => {
51798
+ const stamp = path12.join(dir, ".hunt-version");
51799
+ const files = Object.entries(HUNT_MODULE_FILES);
51800
+ const [current, ...onDisk] = await Promise.all([
51801
+ deps2.readFile(stamp).catch(() => ""),
51802
+ ...files.map(([name21]) => deps2.readFile(path12.join(dir, name21)).catch(() => {
51792
51803
  return;
51793
- }),
51794
- deps2.readFile(stamp).catch(() => "")
51804
+ }))
51795
51805
  ]);
51796
- if (onDisk === PythonHelpers.HUNT_MODULE_SOURCE && current.trim() === PythonHelpers.HUNT_MODULE_VERSION)
51806
+ const changed = files.filter(([, source], index) => onDisk[index] !== source);
51807
+ if (changed.length === 0 && current.trim() === PythonHelpers.HUNT_MODULE_VERSION)
51797
51808
  return dir;
51798
51809
  await deps2.mkdir(dir);
51799
- await deps2.writeFile(target, PythonHelpers.HUNT_MODULE_SOURCE);
51810
+ for (const [name21, source] of changed)
51811
+ await deps2.writeFile(path12.join(dir, name21), source);
51800
51812
  await deps2.writeFile(stamp, PythonHelpers.HUNT_MODULE_VERSION);
51801
51813
  } catch {}
51802
51814
  return dir;
51803
51815
  })();
51804
51816
  ensureTasks.set(dir, task);
51817
+ task.then(() => ensureTasks.delete(dir), () => ensureTasks.delete(dir));
51805
51818
  return task;
51806
51819
  }
51807
51820
  PythonHelpers.ensure = ensure;
@@ -52893,7 +52906,7 @@ async function inspectExecutionRuntime(cwd, env = process.env, options) {
52893
52906
  managedVenvPath: environment.selection.managedVenvPath,
52894
52907
  discovered,
52895
52908
  missing: expected.filter((command) => !discovered.includes(command)),
52896
- helperAvailable: await Bun.file(path15.join(helperDir, "hunt.py")).exists(),
52909
+ helperAvailable: (await Promise.all(Object.keys(HUNT_MODULE_FILES).map((name21) => Bun.file(path15.join(helperDir, name21)).exists()))).every(Boolean),
52897
52910
  helperDir
52898
52911
  };
52899
52912
  }
@@ -52902,6 +52915,7 @@ var init_inspect = __esm(() => {
52902
52915
  init_environment();
52903
52916
  init_python();
52904
52917
  init_python_helpers();
52918
+ init_hunt_modules();
52905
52919
  });
52906
52920
 
52907
52921
  // ../execution-runtime/src/index.ts
@@ -62183,7 +62197,7 @@ Ctrl+C stops the gateway.`);
62183
62197
  async function handleCli() {
62184
62198
  const args = process.argv.slice(2);
62185
62199
  if (args.includes("--version") || args.includes("-v")) {
62186
- console.log("1.0.201");
62200
+ console.log("1.0.202");
62187
62201
  process.exit(0);
62188
62202
  }
62189
62203
  if (args[0] === "dashboard") {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kifaru",
3
- "version": "1.0.201",
4
- "gitHead": "f34d6f06db9f2e0011e4d2925046342d0ecbe330",
3
+ "version": "1.0.202",
4
+ "gitHead": "f0ed02a29c377ac2fb3f4fadac922f37302c5992",
5
5
  "description": "Kifaru security agent on the Pi runtime",
6
6
  "type": "module",
7
7
  "license": "MIT",