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.
- package/kifaru-pi.js +136 -122
- 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/
|
|
51519
|
-
|
|
51520
|
-
|
|
51521
|
-
|
|
51522
|
-
|
|
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"
|
|
51532
|
-
print(r.status_code, r.text
|
|
51533
|
-
|
|
51534
|
-
|
|
51535
|
-
|
|
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
|
-
|
|
51561
|
-
import
|
|
51562
|
-
import
|
|
51563
|
-
import
|
|
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
|
|
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
|
|
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
|
|
51660
|
-
return Resp(
|
|
51661
|
-
|
|
51662
|
-
except urllib.error.HTTPError as
|
|
51663
|
-
return Resp(
|
|
51664
|
-
|
|
51665
|
-
|
|
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(
|
|
51677
|
-
|
|
51678
|
-
|
|
51679
|
-
return (base64.urlsafe_b64decode if urlsafe else base64.b64decode)(
|
|
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(
|
|
51683
|
-
if isinstance(
|
|
51684
|
-
|
|
51685
|
-
|
|
51686
|
-
return
|
|
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
|
|
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
|
-
|
|
51697
|
-
return {"header": header, "payload": payload, "signature":
|
|
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({
|
|
51713
|
-
except Exception as
|
|
51714
|
-
return ["__error__ %s: %s" % (type(
|
|
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
|
-
|
|
51730
|
-
|
|
51731
|
-
|
|
51732
|
-
|
|
51733
|
-
|
|
51734
|
-
|
|
51735
|
-
|
|
51736
|
-
|
|
51737
|
-
|
|
51738
|
-
|
|
51739
|
-
|
|
51740
|
-
|
|
51741
|
-
|
|
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
|
|
51791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
4
|
-
"gitHead": "
|
|
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",
|