pywrit 0.2.8__tar.gz → 0.2.9__tar.gz
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.
- {pywrit-0.2.8/src/pywrit.egg-info → pywrit-0.2.9}/PKG-INFO +13 -2
- {pywrit-0.2.8 → pywrit-0.2.9}/README.md +12 -1
- {pywrit-0.2.8 → pywrit-0.2.9}/pyproject.toml +1 -1
- {pywrit-0.2.8 → pywrit-0.2.9}/src/pywrit/cli.py +47 -3
- {pywrit-0.2.8 → pywrit-0.2.9/src/pywrit.egg-info}/PKG-INFO +13 -2
- {pywrit-0.2.8 → pywrit-0.2.9}/tests/test_scan_tsjs.py +66 -3
- {pywrit-0.2.8 → pywrit-0.2.9}/LICENSE +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/setup.cfg +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/src/pywrit/__init__.py +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/src/pywrit/client.py +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/src/pywrit/errors.py +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/src/pywrit.egg-info/SOURCES.txt +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/src/pywrit.egg-info/dependency_links.txt +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/src/pywrit.egg-info/entry_points.txt +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/src/pywrit.egg-info/requires.txt +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/src/pywrit.egg-info/top_level.txt +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/tests/test_client.py +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/tests/test_scan_json.py +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/tests/test_scan_python.py +0 -0
- {pywrit-0.2.8 → pywrit-0.2.9}/tests/test_wrap.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pywrit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Python client + CLI for Writ — the gate before the write
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/withwrit/pywrit
|
|
@@ -154,7 +154,11 @@ Python scanning never needs it.
|
|
|
154
154
|
TS/JS is **scan-only**: findings are listed with verbs for your policy file,
|
|
155
155
|
but `writ scan --apply` never rewrites TS/JS files — gate those by hand.
|
|
156
156
|
Covered patterns: `fetch`/`axios` writes, `fs` writes, SQL through knex-style
|
|
157
|
-
clients, Prisma writes,
|
|
157
|
+
clients, Prisma writes, JS SDK calls such as `stripe.refunds.create(...)`, and
|
|
158
|
+
generated REST resource clients: `create` / `update` / `patch` / `remove`
|
|
159
|
+
methods (plus their `...WithHttpInfo` variants) that build request options
|
|
160
|
+
with an explicit write HTTP method and route them through the client request
|
|
161
|
+
pipeline (twilio-style generated clients, e.g. `twilio-node`).
|
|
158
162
|
(Still not covered: the *Python* SDK equivalents like `stripe.Refund.create(...)`
|
|
159
163
|
— see "Not covered" above.)
|
|
160
164
|
|
|
@@ -177,6 +181,13 @@ job summary. The check fails when an ungated finding meets your
|
|
|
177
181
|
No API key needed. Full reference: [docs/github-action.md](docs/github-action.md) ·
|
|
178
182
|
example workflow: [examples/github-action/writ-scan.yml](examples/github-action/writ-scan.yml)
|
|
179
183
|
|
|
184
|
+
## GitHub App
|
|
185
|
+
|
|
186
|
+
Install the [Writ GitHub App](https://docs.withwrit.com/git-hub-app) on selected
|
|
187
|
+
repositories: every push and pull request is scanned for ungated write-sites,
|
|
188
|
+
with results as a check run on the PR and in the
|
|
189
|
+
[portal](https://app.withwrit.com), where you manage policy, keys, and the audit log.
|
|
190
|
+
|
|
180
191
|
## MCP server (`writ-mcp`)
|
|
181
192
|
|
|
182
193
|
Give any MCP-compatible agent point-of-action policy checks. `writ-mcp` is a
|
|
@@ -132,7 +132,11 @@ Python scanning never needs it.
|
|
|
132
132
|
TS/JS is **scan-only**: findings are listed with verbs for your policy file,
|
|
133
133
|
but `writ scan --apply` never rewrites TS/JS files — gate those by hand.
|
|
134
134
|
Covered patterns: `fetch`/`axios` writes, `fs` writes, SQL through knex-style
|
|
135
|
-
clients, Prisma writes,
|
|
135
|
+
clients, Prisma writes, JS SDK calls such as `stripe.refunds.create(...)`, and
|
|
136
|
+
generated REST resource clients: `create` / `update` / `patch` / `remove`
|
|
137
|
+
methods (plus their `...WithHttpInfo` variants) that build request options
|
|
138
|
+
with an explicit write HTTP method and route them through the client request
|
|
139
|
+
pipeline (twilio-style generated clients, e.g. `twilio-node`).
|
|
136
140
|
(Still not covered: the *Python* SDK equivalents like `stripe.Refund.create(...)`
|
|
137
141
|
— see "Not covered" above.)
|
|
138
142
|
|
|
@@ -155,6 +159,13 @@ job summary. The check fails when an ungated finding meets your
|
|
|
155
159
|
No API key needed. Full reference: [docs/github-action.md](docs/github-action.md) ·
|
|
156
160
|
example workflow: [examples/github-action/writ-scan.yml](examples/github-action/writ-scan.yml)
|
|
157
161
|
|
|
162
|
+
## GitHub App
|
|
163
|
+
|
|
164
|
+
Install the [Writ GitHub App](https://docs.withwrit.com/git-hub-app) on selected
|
|
165
|
+
repositories: every push and pull request is scanned for ungated write-sites,
|
|
166
|
+
with results as a check run on the PR and in the
|
|
167
|
+
[portal](https://app.withwrit.com), where you manage policy, keys, and the audit log.
|
|
168
|
+
|
|
158
169
|
## MCP server (`writ-mcp`)
|
|
159
170
|
|
|
160
171
|
Give any MCP-compatible agent point-of-action policy checks. `writ-mcp` is a
|
|
@@ -860,7 +860,10 @@ _TSJS_HTTP_VERBS = "post|put|patch|delete|del"
|
|
|
860
860
|
# Patterns are dotted callee chains (after import aliasing and dropping a
|
|
861
861
|
# leading `this`): `a|b` alternatives, `*` one segment, `**` any segments.
|
|
862
862
|
# Conditions: None, "method" (a literal write method in an options object
|
|
863
|
-
# or as a leading string arg), "sql" (first arg is a literal SQL write)
|
|
863
|
+
# or as a leading string arg), "sql" (first arg is a literal SQL write),
|
|
864
|
+
# "resource_write" (generated REST resource method: the enclosing
|
|
865
|
+
# create/update/patch/remove-style method routes a request that carries an
|
|
866
|
+
# explicit write HTTP method; reads never match).
|
|
864
867
|
_TSJS_RULES = (
|
|
865
868
|
("child-process", "exec",
|
|
866
869
|
("child_process.exec|execSync|execFile|execFileSync|spawn|spawnSync|"
|
|
@@ -886,6 +889,14 @@ _TSJS_RULES = (
|
|
|
886
889
|
("http-config-write", "http",
|
|
887
890
|
("axios|got|ky|superagent", "axios|got|ky.request",
|
|
888
891
|
"http|https.request"), "method"),
|
|
892
|
+
# Generated REST resource clients (twilio-style): resource methods named
|
|
893
|
+
# create/update/patch/remove (+ ...WithHttpInfo) that build request
|
|
894
|
+
# options with an explicit write HTTP method and route them through the
|
|
895
|
+
# client request pipeline (Version.request / Domain.request). The
|
|
896
|
+
# callee is the pipeline, so the pattern is unanchored and the condition
|
|
897
|
+
# does the work: it never fires on method: "get" or read-named methods
|
|
898
|
+
# (fetch/list/page/stream/each).
|
|
899
|
+
("resource-write", "http", ("**",), "resource_write"),
|
|
889
900
|
)
|
|
890
901
|
|
|
891
902
|
|
|
@@ -1176,7 +1187,37 @@ def _tsjs_has_write_method(call, f):
|
|
|
1176
1187
|
return False
|
|
1177
1188
|
|
|
1178
1189
|
|
|
1179
|
-
|
|
1190
|
+
_TSJS_RESOURCE_WRITE_NAMES = frozenset(
|
|
1191
|
+
("create", "update", "patch", "remove", "del", "delete"))
|
|
1192
|
+
_TSJS_HTTPINFO_SUFFIX = "withhttpinfo"
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def _tsjs_resource_write_func(func_name):
|
|
1196
|
+
"""True for generated-REST-resource write methods: create/update/patch/
|
|
1197
|
+
remove (plus del/delete variants) and their ...WithHttpInfo forms.
|
|
1198
|
+
Read-style names (fetch/list/page/stream/each/get) never match."""
|
|
1199
|
+
if not func_name:
|
|
1200
|
+
return False
|
|
1201
|
+
n = func_name.lower()
|
|
1202
|
+
if n.endswith(_TSJS_HTTPINFO_SUFFIX):
|
|
1203
|
+
n = n[:-len(_TSJS_HTTPINFO_SUFFIX)]
|
|
1204
|
+
return n in _TSJS_RESOURCE_WRITE_NAMES
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def _tsjs_resource_write(call, func_name, f):
|
|
1208
|
+
"""Generated-resource write: inside a create/update/patch/remove-style
|
|
1209
|
+
method, a request that carries an explicit write HTTP method (a literal
|
|
1210
|
+
`method: "post"`-style options key, or the method as a leading string
|
|
1211
|
+
arg) routed through a call. The callee is the request pipeline and may
|
|
1212
|
+
be any name (operationVersion.update, this.request, Version.fetch for
|
|
1213
|
+
DELETE-that-returns-a-body), so only the literal decides write-vs-read:
|
|
1214
|
+
`method: "get"` is never a write and read-named enclosing methods never
|
|
1215
|
+
match."""
|
|
1216
|
+
return _tsjs_resource_write_func(func_name) and \
|
|
1217
|
+
_tsjs_has_write_method(call, f)
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def _tsjs_classify(call, chain, f, func_name):
|
|
1180
1221
|
for rid, kind, pats, cond in _TSJS_COMPILED:
|
|
1181
1222
|
if not any(_tsjs_glob(p, chain) for p in pats):
|
|
1182
1223
|
continue
|
|
@@ -1187,6 +1228,9 @@ def _tsjs_classify(call, chain, f):
|
|
|
1187
1228
|
lit = f.literal(args[0]) if args else None
|
|
1188
1229
|
if lit is None or not _TSJS_SQL_WRITE.match(lit):
|
|
1189
1230
|
continue
|
|
1231
|
+
if cond == "resource_write" and \
|
|
1232
|
+
not _tsjs_resource_write(call, func_name, f):
|
|
1233
|
+
continue
|
|
1190
1234
|
return rid, kind
|
|
1191
1235
|
return None
|
|
1192
1236
|
|
|
@@ -1229,7 +1273,7 @@ def _tsjs_scan_source(src, filename, parser):
|
|
|
1229
1273
|
if chain[-1] in _TSJS_GATE_NAMES:
|
|
1230
1274
|
gated.add(fkey)
|
|
1231
1275
|
continue
|
|
1232
|
-
hit = _tsjs_classify(node, chain, f)
|
|
1276
|
+
hit = _tsjs_classify(node, chain, f, fkey[0])
|
|
1233
1277
|
if hit:
|
|
1234
1278
|
snippet = " ".join(f.text(node).split())[:100]
|
|
1235
1279
|
sites.append((_WriteSite(filename, fkey[0], cls,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pywrit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Python client + CLI for Writ — the gate before the write
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/withwrit/pywrit
|
|
@@ -154,7 +154,11 @@ Python scanning never needs it.
|
|
|
154
154
|
TS/JS is **scan-only**: findings are listed with verbs for your policy file,
|
|
155
155
|
but `writ scan --apply` never rewrites TS/JS files — gate those by hand.
|
|
156
156
|
Covered patterns: `fetch`/`axios` writes, `fs` writes, SQL through knex-style
|
|
157
|
-
clients, Prisma writes,
|
|
157
|
+
clients, Prisma writes, JS SDK calls such as `stripe.refunds.create(...)`, and
|
|
158
|
+
generated REST resource clients: `create` / `update` / `patch` / `remove`
|
|
159
|
+
methods (plus their `...WithHttpInfo` variants) that build request options
|
|
160
|
+
with an explicit write HTTP method and route them through the client request
|
|
161
|
+
pipeline (twilio-style generated clients, e.g. `twilio-node`).
|
|
158
162
|
(Still not covered: the *Python* SDK equivalents like `stripe.Refund.create(...)`
|
|
159
163
|
— see "Not covered" above.)
|
|
160
164
|
|
|
@@ -177,6 +181,13 @@ job summary. The check fails when an ungated finding meets your
|
|
|
177
181
|
No API key needed. Full reference: [docs/github-action.md](docs/github-action.md) ·
|
|
178
182
|
example workflow: [examples/github-action/writ-scan.yml](examples/github-action/writ-scan.yml)
|
|
179
183
|
|
|
184
|
+
## GitHub App
|
|
185
|
+
|
|
186
|
+
Install the [Writ GitHub App](https://docs.withwrit.com/git-hub-app) on selected
|
|
187
|
+
repositories: every push and pull request is scanned for ungated write-sites,
|
|
188
|
+
with results as a check run on the PR and in the
|
|
189
|
+
[portal](https://app.withwrit.com), where you manage policy, keys, and the audit log.
|
|
190
|
+
|
|
180
191
|
## MCP server (`writ-mcp`)
|
|
181
192
|
|
|
182
193
|
Give any MCP-compatible agent point-of-action policy checks. `writ-mcp` is a
|
|
@@ -113,15 +113,16 @@ class FixtureRulesTest(unittest.TestCase):
|
|
|
113
113
|
verbs = __import__("json").loads(policy.read_text())
|
|
114
114
|
self.assertEqual(code, 0)
|
|
115
115
|
n = len(_expected())
|
|
116
|
-
self.assertIn("ts/js files scanned:
|
|
117
|
-
self.assertIn("ts/js write sites: %d in
|
|
116
|
+
self.assertIn("ts/js files scanned: 9 skipped: 0 (scan only, no --apply)", out)
|
|
117
|
+
self.assertIn("ts/js write sites: %d in 18 function(s)" % n, out)
|
|
118
118
|
self.assertIn("ts/js gated: 1/%d" % n, out) # gated.ts calls writCheck
|
|
119
119
|
self.assertIn(" payments/refunds.service.ts:10 [sdk] stripe-write "
|
|
120
120
|
"issueRefund(): stripe.refunds.create({ charge: chargeId })", out)
|
|
121
121
|
self.assertIn(" http.ts:10 [http] fetch-write createOrder(): fetch(url, "
|
|
122
122
|
"{ method: \"POST\", body: JSON.stringify(body) })", out)
|
|
123
123
|
for v in ("payments.refund", "payments.record", "payments.export",
|
|
124
|
-
"storage.save", "storage.create", "http.create", "legacy.write"
|
|
124
|
+
"storage.save", "storage.create", "http.create", "legacy.write",
|
|
125
|
+
"rest.create", "rest.update", "rest.delete"):
|
|
125
126
|
self.assertEqual(verbs.get(v), "require_grant", v)
|
|
126
127
|
self.assertIn("nothing to instrument (ts/js findings are scan-only", out)
|
|
127
128
|
|
|
@@ -238,6 +239,68 @@ class FileWalkTest(unittest.TestCase):
|
|
|
238
239
|
self.assertEqual(got, ["src/a.ts"])
|
|
239
240
|
|
|
240
241
|
|
|
242
|
+
class ResourceWriteTest(unittest.TestCase):
|
|
243
|
+
"""Regression for generated REST resource clients (twilio-style): the
|
|
244
|
+
fixture has a version request pipeline plus a resource class and a
|
|
245
|
+
factory-pattern list whose create/update/patch/remove methods route
|
|
246
|
+
requests with an explicit write HTTP method. Only those methods are
|
|
247
|
+
findings; fetch/list/get reads (even through pipeline calls) are not."""
|
|
248
|
+
|
|
249
|
+
EXPECTED = {
|
|
250
|
+
("rest/account.ts", 52, "resource-write"), # update()
|
|
251
|
+
("rest/account.ts", 56, "resource-write"), # updateWithHttpInfo()
|
|
252
|
+
("rest/account.ts", 60, "resource-write"), # remove() via Version.fetch
|
|
253
|
+
("rest/account.ts", 64, "resource-write"), # removeWithHttpInfo()
|
|
254
|
+
("rest/account.ts", 68, "resource-write"), # patch()
|
|
255
|
+
("rest/message.ts", 14, "resource-write"), # factory create()
|
|
256
|
+
("rest/message.ts", 18, "resource-write"), # factory remove()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@unittest.skipUnless(HAVE_EXTRA, "polyglot extra not installed")
|
|
260
|
+
def test_fixture_layout_flags_writes_not_reads(self):
|
|
261
|
+
with tempfile.TemporaryDirectory() as td:
|
|
262
|
+
root = _copy_fixtures(pathlib.Path(td) / "repo")
|
|
263
|
+
code, out = _run_scan(root)
|
|
264
|
+
self.assertEqual(code, 0)
|
|
265
|
+
rest = {f for f in _findings(out) if f[0].startswith("rest/")}
|
|
266
|
+
self.assertEqual(rest, self.EXPECTED, out)
|
|
267
|
+
|
|
268
|
+
@unittest.skipUnless(HAVE_EXTRA, "polyglot extra not installed")
|
|
269
|
+
def test_json_mode_reports_resource_finding(self):
|
|
270
|
+
with tempfile.TemporaryDirectory() as td:
|
|
271
|
+
root = pathlib.Path(td) / "repo"
|
|
272
|
+
(root / "rest").mkdir(parents=True)
|
|
273
|
+
(root / "rest" / "account.ts").write_text(
|
|
274
|
+
"class Version {\n"
|
|
275
|
+
" update(opts: any) { return this.request(opts); }\n"
|
|
276
|
+
"}\n"
|
|
277
|
+
"class AccountContext {\n"
|
|
278
|
+
" constructor(private version: Version) {}\n"
|
|
279
|
+
" update() {\n"
|
|
280
|
+
" return this.version.update({ uri: \"/x\", method: \"post\" });\n"
|
|
281
|
+
" }\n"
|
|
282
|
+
" fetch() {\n"
|
|
283
|
+
" return this.version.fetch({ uri: \"/x\", method: \"get\" });\n"
|
|
284
|
+
" }\n"
|
|
285
|
+
"}\n")
|
|
286
|
+
buf = io.StringIO()
|
|
287
|
+
with contextlib.redirect_stdout(buf):
|
|
288
|
+
code = cli.scan_cmd(_args(root, pathlib.Path(td) / "policy.json",
|
|
289
|
+
format="json"))
|
|
290
|
+
out = buf.getvalue()
|
|
291
|
+
self.assertEqual(code, 0)
|
|
292
|
+
doc = __import__("json").loads(out)
|
|
293
|
+
self.assertEqual(doc["files_scanned"], 1)
|
|
294
|
+
findings = doc["findings"]
|
|
295
|
+
self.assertEqual(len(findings), 1, doc)
|
|
296
|
+
f = findings[0]
|
|
297
|
+
self.assertEqual((f["file"], f["line"], f["function"], f["verb"]),
|
|
298
|
+
("rest/account.ts", 7, "update", "rest.update"))
|
|
299
|
+
self.assertEqual((f["kind"], f["language"], f["gated"]),
|
|
300
|
+
("http", "tsjs", False))
|
|
301
|
+
self.assertIn('method: "post"', f["snippet"])
|
|
302
|
+
|
|
303
|
+
|
|
241
304
|
class MissingExtraTest(unittest.TestCase):
|
|
242
305
|
def test_hint_once_and_python_still_scanned(self):
|
|
243
306
|
with tempfile.TemporaryDirectory() as td:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|