sloptic 1.1.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.
- sloptic/__init__.py +7 -0
- sloptic/aggregate.py +120 -0
- sloptic/auth.py +796 -0
- sloptic/baas.py +157 -0
- sloptic/browser.py +1377 -0
- sloptic/catalog/performance/perf-cache-001.yaml +15 -0
- sloptic/catalog/performance/perf-compress-001.yaml +13 -0
- sloptic/catalog/performance/perf-cwv-001.yaml +14 -0
- sloptic/catalog/performance/perf-cwv-002.yaml +17 -0
- sloptic/catalog/performance/perf-load-001.yaml +12 -0
- sloptic/catalog/performance/perf-loadtime-001.yaml +12 -0
- sloptic/catalog/performance/perf-requests-001.yaml +12 -0
- sloptic/catalog/performance/perf-ttfb-001.yaml +13 -0
- sloptic/catalog/performance/perf-ttfb-002.yaml +14 -0
- sloptic/catalog/performance/perf-ttfb-003.yaml +14 -0
- sloptic/catalog/performance/perf-weight-001.yaml +14 -0
- sloptic/catalog/performance/perf-weight-002.yaml +14 -0
- sloptic/catalog/qa/qa-a11y-001.yaml +23 -0
- sloptic/catalog/qa/qa-a11y-002.yaml +16 -0
- sloptic/catalog/qa/qa-backnav-001.yaml +15 -0
- sloptic/catalog/qa/qa-chunk-001.yaml +14 -0
- sloptic/catalog/qa/qa-console-001.yaml +12 -0
- sloptic/catalog/qa/qa-crash-010.yaml +17 -0
- sloptic/catalog/qa/qa-ctype-001.yaml +12 -0
- sloptic/catalog/qa/qa-deadctrl-001.yaml +16 -0
- sloptic/catalog/qa/qa-deeplink-001.yaml +16 -0
- sloptic/catalog/qa/qa-devbuild-001.yaml +47 -0
- sloptic/catalog/qa/qa-errhyg-001.yaml +12 -0
- sloptic/catalog/qa/qa-http-001.yaml +13 -0
- sloptic/catalog/qa/qa-http-002.yaml +13 -0
- sloptic/catalog/qa/qa-input-001.yaml +16 -0
- sloptic/catalog/qa/qa-integrity-001.yaml +12 -0
- sloptic/catalog/qa/qa-integrity-002.yaml +14 -0
- sloptic/catalog/qa/qa-links-001.yaml +16 -0
- sloptic/catalog/qa/qa-noerror-001.yaml +17 -0
- sloptic/catalog/qa/qa-race-001.yaml +12 -0
- sloptic/catalog/qa/qa-race-002.yaml +16 -0
- sloptic/catalog/qa/qa-seo-001.yaml +14 -0
- sloptic/catalog/qa/qa-staleui-001.yaml +16 -0
- sloptic/catalog/security/sec-authbypass-001.yaml +14 -0
- sloptic/catalog/security/sec-backend-001.yaml +14 -0
- sloptic/catalog/security/sec-backend-002.yaml +20 -0
- sloptic/catalog/security/sec-backend-003.yaml +24 -0
- sloptic/catalog/security/sec-cmdi-001.yaml +13 -0
- sloptic/catalog/security/sec-cors-001.yaml +14 -0
- sloptic/catalog/security/sec-csp-001.yaml +15 -0
- sloptic/catalog/security/sec-csrf-001.yaml +10 -0
- sloptic/catalog/security/sec-debug-001.yaml +11 -0
- sloptic/catalog/security/sec-deps-001.yaml +15 -0
- sloptic/catalog/security/sec-domxss-001.yaml +10 -0
- sloptic/catalog/security/sec-dos-001.yaml +12 -0
- sloptic/catalog/security/sec-exposure-001.yaml +12 -0
- sloptic/catalog/security/sec-exposure-002.yaml +13 -0
- sloptic/catalog/security/sec-exposure-003.yaml +13 -0
- sloptic/catalog/security/sec-exposure-004.yaml +12 -0
- sloptic/catalog/security/sec-exposure-005.yaml +14 -0
- sloptic/catalog/security/sec-exposure-006.yaml +11 -0
- sloptic/catalog/security/sec-exposure-007.yaml +18 -0
- sloptic/catalog/security/sec-exposure-008.yaml +25 -0
- sloptic/catalog/security/sec-filterinj-001.yaml +28 -0
- sloptic/catalog/security/sec-headers-001.yaml +12 -0
- sloptic/catalog/security/sec-headers-002.yaml +19 -0
- sloptic/catalog/security/sec-headers-003.yaml +12 -0
- sloptic/catalog/security/sec-headers-004.yaml +12 -0
- sloptic/catalog/security/sec-headers-005.yaml +12 -0
- sloptic/catalog/security/sec-headers-006.yaml +12 -0
- sloptic/catalog/security/sec-hosthdr-001.yaml +12 -0
- sloptic/catalog/security/sec-idor-001.yaml +10 -0
- sloptic/catalog/security/sec-idor-002.yaml +11 -0
- sloptic/catalog/security/sec-idor-003.yaml +11 -0
- sloptic/catalog/security/sec-idor-004.yaml +12 -0
- sloptic/catalog/security/sec-idor-005.yaml +19 -0
- sloptic/catalog/security/sec-lfi-001.yaml +13 -0
- sloptic/catalog/security/sec-mixed-001.yaml +13 -0
- sloptic/catalog/security/sec-ratelimit-001.yaml +13 -0
- sloptic/catalog/security/sec-redirect-001.yaml +10 -0
- sloptic/catalog/security/sec-secrets-001.yaml +13 -0
- sloptic/catalog/security/sec-secrets-002.yaml +12 -0
- sloptic/catalog/security/sec-session-001.yaml +11 -0
- sloptic/catalog/security/sec-session-002.yaml +11 -0
- sloptic/catalog/security/sec-session-003.yaml +13 -0
- sloptic/catalog/security/sec-session-004.yaml +11 -0
- sloptic/catalog/security/sec-session-005.yaml +11 -0
- sloptic/catalog/security/sec-split-001.yaml +12 -0
- sloptic/catalog/security/sec-sqli-001.yaml +12 -0
- sloptic/catalog/security/sec-sqli-002.yaml +12 -0
- sloptic/catalog/security/sec-sqli-003.yaml +12 -0
- sloptic/catalog/security/sec-sqli-004.yaml +17 -0
- sloptic/catalog/security/sec-sqli-005.yaml +12 -0
- sloptic/catalog/security/sec-ssrf-001.yaml +10 -0
- sloptic/catalog/security/sec-ssti-001.yaml +13 -0
- sloptic/catalog/security/sec-upload-001.yaml +12 -0
- sloptic/catalog/security/sec-upload-002.yaml +15 -0
- sloptic/catalog/security/sec-xss-001.yaml +19 -0
- sloptic/catalog/security/sec-xss-002.yaml +17 -0
- sloptic/catalog/security/sec-xxe-001.yaml +10 -0
- sloptic/catalog.py +69 -0
- sloptic/cli.py +362 -0
- sloptic/deploy.py +266 -0
- sloptic/depscan.py +66 -0
- sloptic/discovery.py +1355 -0
- sloptic/ingest.py +84 -0
- sloptic/jsmine.py +59 -0
- sloptic/jsonl.py +22 -0
- sloptic/net.py +106 -0
- sloptic/oob.py +68 -0
- sloptic/openapi.py +164 -0
- sloptic/perf.py +71 -0
- sloptic/pipeline.py +300 -0
- sloptic/platform_id.py +138 -0
- sloptic/probes.py +5483 -0
- sloptic/provenance.py +118 -0
- sloptic/reportcard.py +424 -0
- sloptic/safety.py +64 -0
- sloptic/schema.py +152 -0
- sloptic/scope.py +45 -0
- sloptic/secretscan.py +218 -0
- sloptic/vendor/axe.min.js +12 -0
- sloptic-1.1.0.dist-info/METADATA +177 -0
- sloptic-1.1.0.dist-info/RECORD +123 -0
- sloptic-1.1.0.dist-info/WHEEL +4 -0
- sloptic-1.1.0.dist-info/entry_points.txt +2 -0
- sloptic-1.1.0.dist-info/licenses/LICENSE +201 -0
sloptic/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Sloptic — a black-box HTTP resilience grader.
|
|
2
|
+
|
|
3
|
+
Deploys a submission to a reachable HTTP base URL, discovers its surface, runs the
|
|
4
|
+
applicable catalog probes, and sums per-probe penalties into a slop score. The deploy step
|
|
5
|
+
is abstracted behind a Deployer so the same pipeline runs against a local subprocess (dev/CI,
|
|
6
|
+
trusted reference apps) or a sandboxed Docker container (production, untrusted submissions).
|
|
7
|
+
"""
|
sloptic/aggregate.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Phase 4 aggregation: sum fired-probe penalties into a slop score, with the composition
|
|
2
|
+
dampers (a variant group fires once at its max; repeated instances within a category decay).
|
|
3
|
+
|
|
4
|
+
- **Variant group fires once.** Probes sharing a `variant_group_id` are one logical flaw probed
|
|
5
|
+
via different syntaxes; if any fire, the group contributes its penalty once (its max), never
|
|
6
|
+
once per syntactic variant.
|
|
7
|
+
- **Diminishing returns within a category.** Repeated fired instances in the same category have
|
|
8
|
+
decaying marginal penalty (the worst counts full; each additional one at `decay**i`), so a
|
|
9
|
+
class of mistake is noted with breadth rather than multiplied linearly.
|
|
10
|
+
|
|
11
|
+
Per-bundle ordering (security >> qa > performance) is NOT a runtime multiplier here — it is
|
|
12
|
+
encoded in the per-probe penalty magnitudes (calibration), so applying it again would
|
|
13
|
+
double-count.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections import defaultdict
|
|
18
|
+
|
|
19
|
+
from .schema import Outcome
|
|
20
|
+
|
|
21
|
+
CATEGORY_DECAY = 0.6
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _damped_total(counted: list[Outcome], decay: float) -> float:
|
|
25
|
+
"""The composition dampers applied to a list of outcomes treated as fired: a variant group counts
|
|
26
|
+
once (its highest-penalty member), then per-category diminishing returns (sorted desc, penalty *
|
|
27
|
+
decay**i); categories sum in full. Shared by the raw slop score and the per-axis normalization."""
|
|
28
|
+
groups: dict[str, Outcome] = {}
|
|
29
|
+
singles: list[Outcome] = []
|
|
30
|
+
for o in counted:
|
|
31
|
+
if o.variant_group_id:
|
|
32
|
+
cur = groups.get(o.variant_group_id)
|
|
33
|
+
if cur is None or o.penalty > cur.penalty:
|
|
34
|
+
groups[o.variant_group_id] = o
|
|
35
|
+
else:
|
|
36
|
+
singles.append(o)
|
|
37
|
+
|
|
38
|
+
by_category: dict[str, list[int]] = defaultdict(list)
|
|
39
|
+
for o in (*singles, *groups.values()):
|
|
40
|
+
by_category[o.category].append(o.penalty)
|
|
41
|
+
|
|
42
|
+
total = 0.0
|
|
43
|
+
for penalties in by_category.values():
|
|
44
|
+
for i, penalty in enumerate(sorted(penalties, reverse=True)):
|
|
45
|
+
total += penalty * (decay ** i)
|
|
46
|
+
return total
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compute_slop_score(outcomes: list[Outcome], decay: float = CATEGORY_DECAY) -> int:
|
|
50
|
+
fired = [o for o in outcomes if o.outcome == "slop_detected"]
|
|
51
|
+
return round(_damped_total(fired, decay))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def compute_axis_slop(outcomes: list[Outcome], decay: float = CATEGORY_DECAY) -> dict[str, int]:
|
|
55
|
+
"""The damped slop subtotal per bundle (security / qa / performance) — unbounded, lower = better, in
|
|
56
|
+
the SAME units as slop_score. A pure decomposition, not a reweighting: every category belongs to one
|
|
57
|
+
bundle, so the subtotals sum to slop_score. No caps, no axis multipliers, no 0-100 normalization."""
|
|
58
|
+
fired = [o for o in outcomes if o.outcome == "slop_detected"]
|
|
59
|
+
by_bundle: dict[str, list[Outcome]] = defaultdict(list)
|
|
60
|
+
for o in fired:
|
|
61
|
+
by_bundle[o.bundle].append(o)
|
|
62
|
+
return {bundle: round(_damped_total(outs, decay)) for bundle, outs in by_bundle.items()}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def coverage_metrics(outcomes: list[Outcome]) -> dict:
|
|
66
|
+
"""How much of the test battery actually APPLIED to this app — the fuzzer's-eye complement to the
|
|
67
|
+
observed-surface fingerprint, and a first-class calibration input. A probe 'ran' if it was applicable
|
|
68
|
+
(fired OR came back clean); it's 'n/a' when the app had no surface for it (no login form -> auth probes
|
|
69
|
+
n/a, no https -> HSTS n/a). Measured per PROBE (fan-out collapsed to the strongest status), and per
|
|
70
|
+
KIND (category), because a pile of N/A kinds — injection/upload/auth all n/a — is exactly the
|
|
71
|
+
'blind, or genuinely tiny' signal parity has to weigh: a low slop score means nothing if 60% of the
|
|
72
|
+
battery never applied."""
|
|
73
|
+
rank = {"not_applicable": 0, "clean": 1, "slop_detected": 2}
|
|
74
|
+
best: dict[str, tuple] = {} # probe_id -> (status, category, bundle), strongest status across fan-out
|
|
75
|
+
for o in outcomes:
|
|
76
|
+
if o.probe_id not in best or rank[o.outcome] > rank[best[o.probe_id][0]]:
|
|
77
|
+
best[o.probe_id] = (o.outcome, o.category, o.bundle)
|
|
78
|
+
total = len(best)
|
|
79
|
+
applicable = sum(1 for status, _, _ in best.values() if status != "not_applicable")
|
|
80
|
+
by_kind: dict[str, dict] = {} # category -> {ran, na, bundle}
|
|
81
|
+
for status, cat, bundle in best.values():
|
|
82
|
+
d = by_kind.setdefault(cat, {"ran": 0, "na": 0, "bundle": bundle})
|
|
83
|
+
d["na" if status == "not_applicable" else "ran"] += 1
|
|
84
|
+
ran_kinds = sorted(k for k, d in by_kind.items() if d["ran"]) # kind applied on ≥1 probe
|
|
85
|
+
na_kinds = sorted(k for k, d in by_kind.items() if d["ran"] == 0) # kind entirely n/a
|
|
86
|
+
# the exact probe_ids that APPLIED here (fired or clean). Aggregated across a batch, the catalog minus
|
|
87
|
+
# the union of these is the set of probes that were N/A on EVERY app — never reached their target.
|
|
88
|
+
applied = sorted(pid for pid, (status, _, _) in best.items() if status != "not_applicable")
|
|
89
|
+
# per-kind N/A REASON telemetry: for a kind that was entirely N/A, surface WHY it couldn't run (e.g.
|
|
90
|
+
# access-control -> "couldn't establish two accounts"). Turns a silent no-op into visible, honest
|
|
91
|
+
# coverage — the credential reports what it TRIED and why it didn't apply, not just a blank. A probe
|
|
92
|
+
# sets ctx.evidence["na_reason"] at the precondition it fell out of; the N/A outcome carries it here.
|
|
93
|
+
# PER-KIND keeps only the FIRST reason, and that masking cost a real answer: on v11 the session cluster's
|
|
94
|
+
# cookie probes reported "token auth is sec-session-005's case" while 005 itself recorded a DIFFERENT reason
|
|
95
|
+
# that never surfaced — so 187 apps looked covered and were not. A kind with several probes has several
|
|
96
|
+
# preconditions, and reading one of them is reading a sample of size one. `na_reasons` stays as-is for
|
|
97
|
+
# existing consumers; `na_reasons_by_probe` is the one to analyse from.
|
|
98
|
+
na_reasons: dict[str, str] = {}
|
|
99
|
+
na_reasons_by_probe: dict[str, str] = {}
|
|
100
|
+
for o in outcomes:
|
|
101
|
+
if o.outcome != "not_applicable":
|
|
102
|
+
continue
|
|
103
|
+
reason = (getattr(o, "evidence", None) or {}).get("na_reason")
|
|
104
|
+
if not reason:
|
|
105
|
+
continue
|
|
106
|
+
na_reasons_by_probe[o.probe_id] = reason
|
|
107
|
+
if o.category in na_kinds and o.category not in na_reasons:
|
|
108
|
+
na_reasons[o.category] = reason
|
|
109
|
+
return {
|
|
110
|
+
"probes_total": total,
|
|
111
|
+
"probes_applicable": applicable,
|
|
112
|
+
"probes_na": total - applicable,
|
|
113
|
+
"pct_applicable": round(applicable / total * 100) if total else 0,
|
|
114
|
+
"ran_kinds": ran_kinds,
|
|
115
|
+
"na_kinds": na_kinds,
|
|
116
|
+
"by_kind": by_kind,
|
|
117
|
+
"applied": applied,
|
|
118
|
+
"na_reasons": na_reasons,
|
|
119
|
+
"na_reasons_by_probe": na_reasons_by_probe,
|
|
120
|
+
}
|