ranwhat 0.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.
- ranwhat/__init__.py +3 -0
- ranwhat/__main__.py +7 -0
- ranwhat/catalog.py +250 -0
- ranwhat/clean.py +570 -0
- ranwhat/cli.py +248 -0
- ranwhat/demo/support-copilot.json +60 -0
- ranwhat/demo/well-configured.json +39 -0
- ranwhat/html_report.py +263 -0
- ranwhat/introspect.py +148 -0
- ranwhat/report.py +94 -0
- ranwhat/score.py +420 -0
- ranwhat/usage.py +334 -0
- ranwhat/watch.py +903 -0
- ranwhat-0.1.0.dist-info/METADATA +247 -0
- ranwhat-0.1.0.dist-info/RECORD +19 -0
- ranwhat-0.1.0.dist-info/WHEEL +5 -0
- ranwhat-0.1.0.dist-info/entry_points.txt +2 -0
- ranwhat-0.1.0.dist-info/licenses/LICENSE +21 -0
- ranwhat-0.1.0.dist-info/top_level.txt +1 -0
ranwhat/__init__.py
ADDED
ranwhat/__main__.py
ADDED
ranwhat/catalog.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scope catalog: what each granted permission actually lets an agent do.
|
|
3
|
+
|
|
4
|
+
This is the core of the scan. Introspection tells you an agent holds
|
|
5
|
+
"https://www.googleapis.com/auth/gmail.send". It does not tell you that this
|
|
6
|
+
is an irreversible, externally-visible write that no underwriter will price
|
|
7
|
+
without a human-approval gate. That mapping lives here.
|
|
8
|
+
|
|
9
|
+
Fields per scope:
|
|
10
|
+
label human-readable capability
|
|
11
|
+
authority read | write | financial | destructive
|
|
12
|
+
reversible can the action be undone after the fact
|
|
13
|
+
blast which blast-radius dimension it opens
|
|
14
|
+
why the sentence that goes in the report
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
READ, WRITE, FINANCIAL, DESTRUCTIVE = "read", "write", "financial", "destructive"
|
|
20
|
+
|
|
21
|
+
AUTHORITY_RANK = {READ: 0, WRITE: 1, FINANCIAL: 2, DESTRUCTIVE: 3}
|
|
22
|
+
|
|
23
|
+
# blast-radius dimensions
|
|
24
|
+
MONETARY = "monetary"
|
|
25
|
+
EXTERNAL_COMMS = "external_comms"
|
|
26
|
+
DATA_EGRESS = "data_egress"
|
|
27
|
+
INFRASTRUCTURE = "infrastructure"
|
|
28
|
+
IDENTITY = "identity"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _s(label, authority, reversible, blast, why):
|
|
32
|
+
return {
|
|
33
|
+
"label": label,
|
|
34
|
+
"authority": authority,
|
|
35
|
+
"reversible": reversible,
|
|
36
|
+
"blast": blast,
|
|
37
|
+
"why": why,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
CATALOG = {
|
|
42
|
+
"google": {
|
|
43
|
+
"https://www.googleapis.com/auth/gmail.readonly": _s(
|
|
44
|
+
"Read all mail", READ, True, DATA_EGRESS,
|
|
45
|
+
"Full mailbox read. Every message the agent can see is exfiltratable "
|
|
46
|
+
"by a prompt injection delivered in any one of them."),
|
|
47
|
+
"https://www.googleapis.com/auth/gmail.send": _s(
|
|
48
|
+
"Send mail as the user", WRITE, False, EXTERNAL_COMMS,
|
|
49
|
+
"Can email any external party as the user. Sent mail cannot be recalled."),
|
|
50
|
+
"https://www.googleapis.com/auth/gmail.modify": _s(
|
|
51
|
+
"Read, send, modify and label mail", WRITE, False, EXTERNAL_COMMS,
|
|
52
|
+
"Superset of read+send. Can also hide its own activity by "
|
|
53
|
+
"relabelling or archiving the evidence."),
|
|
54
|
+
"https://mail.google.com/": _s(
|
|
55
|
+
"Full mailbox control including permanent delete", DESTRUCTIVE, False, DATA_EGRESS,
|
|
56
|
+
"Total mailbox authority including irreversible deletion. This is the "
|
|
57
|
+
"broadest Gmail scope that exists."),
|
|
58
|
+
"https://www.googleapis.com/auth/calendar": _s(
|
|
59
|
+
"Read/write calendar", WRITE, True, EXTERNAL_COMMS,
|
|
60
|
+
"Can create events that email external attendees."),
|
|
61
|
+
"https://www.googleapis.com/auth/calendar.readonly": _s(
|
|
62
|
+
"Read calendar", READ, True, DATA_EGRESS,
|
|
63
|
+
"Reveals meeting topics, attendees and internal org structure."),
|
|
64
|
+
"https://www.googleapis.com/auth/drive": _s(
|
|
65
|
+
"Full Drive access", DESTRUCTIVE, False, DATA_EGRESS,
|
|
66
|
+
"Read, write, share and permanently delete any file. Sharing is an "
|
|
67
|
+
"egress path that leaves no trace in most DLP tooling."),
|
|
68
|
+
"https://www.googleapis.com/auth/drive.file": _s(
|
|
69
|
+
"Drive access limited to files the app created", WRITE, True, DATA_EGRESS,
|
|
70
|
+
"Correctly scoped. This is what most Drive integrations should use."),
|
|
71
|
+
"https://www.googleapis.com/auth/drive.readonly": _s(
|
|
72
|
+
"Read all Drive files", READ, True, DATA_EGRESS,
|
|
73
|
+
"Full document corpus is readable, and therefore summarisable into "
|
|
74
|
+
"any outbound channel the agent also holds."),
|
|
75
|
+
"https://www.googleapis.com/auth/contacts": _s(
|
|
76
|
+
"Read/write contacts", WRITE, True, DATA_EGRESS,
|
|
77
|
+
"Contact list is the target list for any outbound abuse."),
|
|
78
|
+
"https://www.googleapis.com/auth/cloud-platform": _s(
|
|
79
|
+
"Full Google Cloud control", DESTRUCTIVE, False, INFRASTRUCTURE,
|
|
80
|
+
"Complete control of the GCP project including billing, IAM and "
|
|
81
|
+
"resource deletion."),
|
|
82
|
+
},
|
|
83
|
+
"github": {
|
|
84
|
+
"repo": _s(
|
|
85
|
+
"Full control of private repositories", WRITE, False, DATA_EGRESS,
|
|
86
|
+
"Read and write all private source. Includes force-push, which can "
|
|
87
|
+
"rewrite history and destroy the audit trail."),
|
|
88
|
+
"public_repo": _s(
|
|
89
|
+
"Write access to public repositories", WRITE, False, DATA_EGRESS,
|
|
90
|
+
"Can publish to public repos. A misdirected commit is a permanent "
|
|
91
|
+
"public disclosure."),
|
|
92
|
+
"delete_repo": _s(
|
|
93
|
+
"Delete repositories", DESTRUCTIVE, False, INFRASTRUCTURE,
|
|
94
|
+
"Irreversible destruction of a repository. Almost never needed by an agent."),
|
|
95
|
+
"admin:org": _s(
|
|
96
|
+
"Full organisation administration", DESTRUCTIVE, False, IDENTITY,
|
|
97
|
+
"Can add and remove org members, i.e. can grant persistence to an attacker."),
|
|
98
|
+
"workflow": _s(
|
|
99
|
+
"Update GitHub Actions workflows", DESTRUCTIVE, False, INFRASTRUCTURE,
|
|
100
|
+
"Can modify CI. A workflow edit is arbitrary code execution with your "
|
|
101
|
+
"CI secrets attached."),
|
|
102
|
+
"write:packages": _s(
|
|
103
|
+
"Publish packages", WRITE, False, INFRASTRUCTURE,
|
|
104
|
+
"Can publish artifacts consumed downstream. Supply-chain reach."),
|
|
105
|
+
"read:org": _s(
|
|
106
|
+
"Read org membership", READ, True, IDENTITY,
|
|
107
|
+
"Low risk on its own."),
|
|
108
|
+
"gist": _s(
|
|
109
|
+
"Create gists", WRITE, False, DATA_EGRESS,
|
|
110
|
+
"A public gist is a one-call exfiltration primitive."),
|
|
111
|
+
},
|
|
112
|
+
"slack": {
|
|
113
|
+
"chat:write": _s(
|
|
114
|
+
"Post messages", WRITE, False, EXTERNAL_COMMS,
|
|
115
|
+
"Can post as the app into any channel it is in. Messages are seen "
|
|
116
|
+
"before they can be deleted."),
|
|
117
|
+
"channels:history": _s(
|
|
118
|
+
"Read public channel history", READ, True, DATA_EGRESS,
|
|
119
|
+
"Full public conversation history is readable."),
|
|
120
|
+
"groups:history": _s(
|
|
121
|
+
"Read private channel history", READ, True, DATA_EGRESS,
|
|
122
|
+
"Private channel content. Usually the most sensitive text in a company."),
|
|
123
|
+
"im:history": _s(
|
|
124
|
+
"Read direct messages", READ, True, DATA_EGRESS,
|
|
125
|
+
"DM content. Rarely justifiable for an agent."),
|
|
126
|
+
"files:read": _s(
|
|
127
|
+
"Read files", READ, True, DATA_EGRESS,
|
|
128
|
+
"All shared files including exports and credentials pasted as snippets."),
|
|
129
|
+
"users:read.email": _s(
|
|
130
|
+
"Read user email addresses", READ, True, IDENTITY,
|
|
131
|
+
"Directory of addressable humans."),
|
|
132
|
+
"admin": _s(
|
|
133
|
+
"Workspace administration", DESTRUCTIVE, False, IDENTITY,
|
|
134
|
+
"Full workspace control."),
|
|
135
|
+
},
|
|
136
|
+
"stripe": {
|
|
137
|
+
"charges:write": _s(
|
|
138
|
+
"Create and capture charges", FINANCIAL, False, MONETARY,
|
|
139
|
+
"Can move customer money. Settled charges are reversible only via "
|
|
140
|
+
"refund, which is a separate, slower, partially-fee-bearing action."),
|
|
141
|
+
"refunds:write": _s(
|
|
142
|
+
"Issue refunds", FINANCIAL, False, MONETARY,
|
|
143
|
+
"Can pay money out. This is the single most commonly abused agent "
|
|
144
|
+
"capability in reported incidents."),
|
|
145
|
+
"transfers:write": _s(
|
|
146
|
+
"Move money to connected accounts", FINANCIAL, False, MONETARY,
|
|
147
|
+
"Outbound transfer authority. Effectively irreversible once settled."),
|
|
148
|
+
"payment_intents:write": _s(
|
|
149
|
+
"Create payment intents", FINANCIAL, True, MONETARY,
|
|
150
|
+
"Initiates payment flows."),
|
|
151
|
+
"customers:read": _s(
|
|
152
|
+
"Read customer records", READ, True, DATA_EGRESS,
|
|
153
|
+
"PII and payment metadata for the full customer base."),
|
|
154
|
+
"customers:write": _s(
|
|
155
|
+
"Modify customer records", WRITE, True, IDENTITY,
|
|
156
|
+
"Can change the email on a customer record, which is an account-takeover "
|
|
157
|
+
"primitive in most billing flows."),
|
|
158
|
+
"all": _s(
|
|
159
|
+
"Unrestricted secret key", DESTRUCTIVE, False, MONETARY,
|
|
160
|
+
"A live secret key with no restrictions. Every Stripe capability, "
|
|
161
|
+
"including payouts, is available to whatever holds this."),
|
|
162
|
+
},
|
|
163
|
+
"aws": {
|
|
164
|
+
"*": _s(
|
|
165
|
+
"Full AWS administrator", DESTRUCTIVE, False, INFRASTRUCTURE,
|
|
166
|
+
"Unrestricted control of the account including IAM, billing and deletion."),
|
|
167
|
+
"s3:*": _s(
|
|
168
|
+
"Full S3 control", DESTRUCTIVE, False, DATA_EGRESS,
|
|
169
|
+
"Read, write, make-public and delete every bucket. Covers both the "
|
|
170
|
+
"egress path and the destruction of the logs that would record it."),
|
|
171
|
+
"s3:GetObject": _s(
|
|
172
|
+
"Read objects", READ, True, DATA_EGRESS,
|
|
173
|
+
"Object read."),
|
|
174
|
+
"s3:PutObject": _s(
|
|
175
|
+
"Write objects", WRITE, True, DATA_EGRESS,
|
|
176
|
+
"Object write."),
|
|
177
|
+
"s3:DeleteObject": _s(
|
|
178
|
+
"Delete objects", DESTRUCTIVE, False, DATA_EGRESS,
|
|
179
|
+
"Irreversible unless versioning is on."),
|
|
180
|
+
"iam:*": _s(
|
|
181
|
+
"Full IAM control", DESTRUCTIVE, False, IDENTITY,
|
|
182
|
+
"Can grant itself any other permission. This makes every other scope "
|
|
183
|
+
"limit on this credential decorative."),
|
|
184
|
+
"ses:SendEmail": _s(
|
|
185
|
+
"Send email", WRITE, False, EXTERNAL_COMMS,
|
|
186
|
+
"Outbound email from your verified domain."),
|
|
187
|
+
"lambda:InvokeFunction": _s(
|
|
188
|
+
"Invoke functions", WRITE, True, INFRASTRUCTURE,
|
|
189
|
+
"Arbitrary invocation of deployed code."),
|
|
190
|
+
},
|
|
191
|
+
"generic": {},
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# Verb inference for scopes not in the catalog. Cloud providers mint new
|
|
196
|
+
# actions constantly; guessing from the verb is far more accurate than
|
|
197
|
+
# inheriting the severity of a broad wildcard entry.
|
|
198
|
+
_READ_VERBS = ("get", "list", "describe", "read", "view", "search", "query", "head")
|
|
199
|
+
_DESTRUCTIVE_VERBS = ("delete", "terminate", "destroy", "remove", "purge", "revoke", "drop")
|
|
200
|
+
_FINANCIAL_HINTS = ("payment", "charge", "refund", "payout", "transfer", "invoice", "billing")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _infer(provider, scope):
|
|
204
|
+
"""Classify an unrecognised scope from its action verb."""
|
|
205
|
+
tail = scope.split(":")[-1].split(".")[-1].split("/")[-1].lower()
|
|
206
|
+
lowered = scope.lower()
|
|
207
|
+
|
|
208
|
+
if any(h in lowered for h in _FINANCIAL_HINTS) and not tail.startswith(_READ_VERBS):
|
|
209
|
+
authority, reversible, blast = FINANCIAL, False, MONETARY
|
|
210
|
+
elif tail.startswith(_DESTRUCTIVE_VERBS):
|
|
211
|
+
authority, reversible, blast = DESTRUCTIVE, False, INFRASTRUCTURE
|
|
212
|
+
elif tail.startswith(_READ_VERBS):
|
|
213
|
+
authority, reversible, blast = READ, True, DATA_EGRESS
|
|
214
|
+
else:
|
|
215
|
+
authority, reversible, blast = WRITE, False, DATA_EGRESS
|
|
216
|
+
|
|
217
|
+
entry = _s(
|
|
218
|
+
scope, authority, reversible, blast,
|
|
219
|
+
"Not in the capability catalog. Classified as %s from its action verb. "
|
|
220
|
+
"Confirm this manually before relying on the score." % authority,
|
|
221
|
+
)
|
|
222
|
+
entry["known"] = False
|
|
223
|
+
return entry
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def lookup(provider, scope):
|
|
227
|
+
"""Resolve a granted scope to its capability entry.
|
|
228
|
+
|
|
229
|
+
An exact catalog hit wins. A granted scope that is itself a wildcard
|
|
230
|
+
(e.g. "s3:*") matches the catalog wildcard. A narrow granted scope is
|
|
231
|
+
NEVER widened to a broad wildcard entry -- being granted s3:ListBucket
|
|
232
|
+
is not the same as being granted s3:*, and scoring it that way would
|
|
233
|
+
make the whole report untrustworthy.
|
|
234
|
+
"""
|
|
235
|
+
prov = CATALOG.get(provider, {})
|
|
236
|
+
if scope in prov:
|
|
237
|
+
entry = dict(prov[scope])
|
|
238
|
+
entry["known"] = True
|
|
239
|
+
return entry
|
|
240
|
+
|
|
241
|
+
if scope.endswith("*"):
|
|
242
|
+
wildcards = sorted([s for s in prov if s.endswith("*")], key=len, reverse=True)
|
|
243
|
+
for pattern in wildcards:
|
|
244
|
+
if scope.startswith(pattern[:-1]):
|
|
245
|
+
entry = dict(prov[pattern])
|
|
246
|
+
entry["known"] = True
|
|
247
|
+
entry["label"] = "%s (matched %s)" % (entry["label"], pattern)
|
|
248
|
+
return entry
|
|
249
|
+
|
|
250
|
+
return _infer(provider, scope)
|