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 ADDED
@@ -0,0 +1,3 @@
1
+ """ranwhat -- flight recorder and authority scanner for AI agents."""
2
+
3
+ __version__ = "0.1.0"
ranwhat/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Allow `python -m ranwhat` as well as the installed `ranwhat` script."""
2
+ import sys
3
+
4
+ from .cli import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
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)