scamscript 0.2.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.
scamscript/verify.py ADDED
@@ -0,0 +1,103 @@
1
+ """Check model quotations against the source script."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from difflib import SequenceMatcher
8
+
9
+ from scamscript.models import ScriptBreakdown
10
+
11
+ QUOTE_PATTERN = re.compile(r"[\"“”'‘’]([^\"“”'‘’]{8,400})[\"“”'‘’]")
12
+ WORD = re.compile(r"[a-z0-9]+")
13
+ MIN_QUOTE_WORDS = 3
14
+ CLOSE_RATIO = 0.75
15
+
16
+ FIELDS = ("opening_hook", "red_flags")
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class Issue:
21
+ field: str
22
+ quote: str
23
+ status: str
24
+ ratio: float = 0.0
25
+
26
+ @property
27
+ def ok(self) -> bool:
28
+ return self.status == "verbatim"
29
+
30
+
31
+ def normalize(text: str) -> str:
32
+ lowered = (
33
+ text.lower()
34
+ .replace("“", '"')
35
+ .replace("”", '"')
36
+ .replace("‘", "'")
37
+ .replace("’", "'")
38
+ .replace("–", "-")
39
+ .replace("—", "-")
40
+ )
41
+ return " ".join(re.sub(r"[^a-z0-9 ]+", " ", lowered).split())
42
+
43
+
44
+ def extract_quotes(value: str) -> list[str]:
45
+ quotes = [match.group(1).strip() for match in QUOTE_PATTERN.finditer(value)]
46
+ return [quote for quote in quotes if len(WORD.findall(quote)) >= MIN_QUOTE_WORDS]
47
+
48
+
49
+ def classify(quote: str, source: str) -> tuple[str, float]:
50
+ """Return (status, ratio) for one quote against the source."""
51
+ needle = normalize(quote)
52
+ if not needle:
53
+ return "missing", 0.0
54
+ hay = normalize(source)
55
+ if needle in hay:
56
+ return "verbatim", 1.0
57
+
58
+ words = needle.split()
59
+ hay_words = hay.split()
60
+ window = len(words)
61
+ step = max(1, window // 3)
62
+ best = 0.0
63
+ for start in range(0, max(1, len(hay_words) - window + 1), step):
64
+ candidate = " ".join(hay_words[start : start + window])
65
+ ratio = SequenceMatcher(None, needle, candidate).ratio()
66
+ if ratio > best:
67
+ best = ratio
68
+ if best >= 0.97:
69
+ break
70
+ if best >= CLOSE_RATIO:
71
+ return "close", round(best, 3)
72
+ return "missing", round(best, 3)
73
+
74
+
75
+ def verify_breakdown(breakdown: ScriptBreakdown, source: str) -> list[Issue]:
76
+ """Check quoted claims in the hook and red flags. Paraphrase without quotes is skipped."""
77
+ issues: list[Issue] = []
78
+ for field in FIELDS:
79
+ value = getattr(breakdown, field)
80
+ values = value if isinstance(value, list) else [value]
81
+ for item in values:
82
+ if not item:
83
+ continue
84
+ quotes = extract_quotes(item)
85
+ if not quotes and field == "opening_hook" and len(WORD.findall(item)) >= 4:
86
+ quotes = [item]
87
+ for quote in quotes:
88
+ status, ratio = classify(quote, source)
89
+ issues.append(
90
+ Issue(field=field, quote=quote.strip(), status=status, ratio=ratio)
91
+ )
92
+ return issues
93
+
94
+
95
+ def summarize(issues: list[Issue]) -> dict[str, int]:
96
+ counts = {"checked": len(issues), "verbatim": 0, "close": 0, "missing": 0}
97
+ for issue in issues:
98
+ counts[issue.status] = counts.get(issue.status, 0) + 1
99
+ return counts
100
+
101
+
102
+ def has_problems(issues: list[Issue]) -> bool:
103
+ return any(issue.status != "verbatim" for issue in issues)
@@ -0,0 +1,278 @@
1
+ Metadata-Version: 2.5
2
+ Name: scamscript
3
+ Version: 0.2.0
4
+ Summary: Reverse-engineer scam scripts and vishing transcripts into structured counter-playbooks
5
+ Project-URL: Homepage, https://github.com/sjdsjadjajdjsa/scamscript
6
+ Project-URL: Repository, https://github.com/sjdsjadjajdjsa/scamscript
7
+ Project-URL: Issues, https://github.com/sjdsjadjajdjsa/scamscript/issues
8
+ Project-URL: Changelog, https://github.com/sjdsjadjajdjsa/scamscript/blob/master/CHANGELOG.md
9
+ Author-email: scamscript contributors <313729361+sjdsjadjajdjsa@users.noreply.github.com>
10
+ Maintainer-email: scamscript contributors <313729361+sjdsjadjajdjsa@users.noreply.github.com>
11
+ License: MIT License
12
+
13
+ Copyright (c) 2026 scamscript contributors
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ License-File: LICENSE
33
+ Keywords: cli,fraud,osint,phishing,scam,security,smishing,social-engineering,transcripts,vishing
34
+ Classifier: Development Status :: 4 - Beta
35
+ Classifier: Environment :: Console
36
+ Classifier: Intended Audience :: Information Technology
37
+ Classifier: Intended Audience :: Science/Research
38
+ Classifier: License :: OSI Approved :: MIT License
39
+ Classifier: Natural Language :: English
40
+ Classifier: Operating System :: OS Independent
41
+ Classifier: Programming Language :: Python :: 3
42
+ Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Programming Language :: Python :: 3.13
44
+ Classifier: Topic :: Security
45
+ Classifier: Typing :: Typed
46
+ Requires-Python: >=3.12
47
+ Requires-Dist: httpx==0.28.1
48
+ Requires-Dist: pydantic==2.14.2
49
+ Requires-Dist: rich==15.0.0
50
+ Requires-Dist: typer==0.27.3
51
+ Provides-Extra: dev
52
+ Requires-Dist: build==1.3.0; extra == 'dev'
53
+ Requires-Dist: pytest==8.4.1; extra == 'dev'
54
+ Requires-Dist: twine==6.1.0; extra == 'dev'
55
+ Description-Content-Type: text/markdown
56
+
57
+ <p align="center">
58
+ <img src="docs/banner.svg" alt="scamscript: reverse-engineer the playbook, not the story" width="100%">
59
+ </p>
60
+
61
+ <p align="center">
62
+ <img src="docs/demo.gif" alt="terminal demo: analyzing a script, then ranking a directory" width="760">
63
+ <br>
64
+ <sub>Recorded against a local stub endpoint, so no API spend.</sub>
65
+ </p>
66
+
67
+ # scamscript
68
+
69
+ [![CI](https://github.com/sjdsjadjajdjsa/scamscript/actions/workflows/ci.yml/badge.svg)](https://github.com/sjdsjadjajdjsa/scamscript/actions/workflows/ci.yml)
70
+ [![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13-3776ab)](pyproject.toml)
71
+ [![License](https://img.shields.io/badge/license-MIT-2a9d8f)](LICENSE)
72
+ [![Scope](https://img.shields.io/badge/scope-security%20research-e63946)](#scope-and-disclaimer)
73
+
74
+ A reverse-engineering analyzer for leaked or public scam call scripts and vishing transcripts.
75
+ Point it at a script and a model takes the fraud apart: the persona, the hook that catches you,
76
+ how trust is manufactured, the pressure applied, what happens when you hesitate, and how to
77
+ counter that exact playbook.
78
+
79
+ > **Security research and fraud-awareness training only.** Output is a model-produced reading of
80
+ > a captured script, not legal advice and not attribution. A script is evidence of a *pattern*;
81
+ > the live scam that reuses it will differ in details.
82
+
83
+ ---
84
+
85
+ ## Why
86
+
87
+ Scripts leak. Call centres dump their flows, victims publish transcripts, researchers collect
88
+ samples. Reading them one by one teaches you that `your account is at risk` is bad, but not how a
89
+ particular operation sequences trust into a transfer. This tool turns each artifact into a
90
+ comparable record, so a corpus can be ranked and a defender's briefing can quote the actual
91
+ mechanics instead of general advice.
92
+
93
+ ## Quickstart
94
+
95
+ ```bash
96
+ pip install scamscript # or, for development:
97
+ # git clone https://github.com/sjdsjadjajdjsa/scamscript && cd scamscript && pip install -e ".[dev]"
98
+
99
+ export API_KEY=... # never committed, never logged
100
+
101
+ scamscript --dry-run analyze --file examples/tech_support_scam.txt # inspect the prompt, spend nothing
102
+ scamscript analyze --file examples/tech_support_scam.txt
103
+ ```
104
+
105
+ Any OpenAI-compatible `/chat/completions` endpoint works. Defaults are Venice with
106
+ `deepseek-v4-1-flash-1017-uncensored`; OpenRouter needs only the model slug:
107
+
108
+ ```bash
109
+ scamscript --base-url https://openrouter.ai/api/v1 \
110
+ --model deepseek/deepseek-v4.1-flash-1017 analyze --file script.txt
111
+ ```
112
+
113
+ ## What it produces
114
+
115
+ Each script renders as a colour-coded panel:
116
+
117
+ ```text
118
+ ╭──────────────────────────── tech_support_scam.txt ────────────────────────────╮
119
+ │ ATTACK TYPE TECH SUPPORT SCAM SOPHISTICATION 6/10 │
120
+ │ │
121
+ │ OPENING HOOK │
122
+ │ "Our telemetry shows your machine has been broadcasting error ..." │
123
+ │ │
124
+ │ RED FLAGS │
125
+ │ ✗ Unsolicited call claiming Windows telemetry │
126
+ │ ✗ Caller asks for a Remote Desktop session ID │
127
+ ╰────────────────────────────────── 7 red flags ───────────────────────────────╯
128
+ ```
129
+
130
+ Fields: `attack_type`, `persona`, `opening_hook`, `trust_building_moves`, `urgency_tactics`,
131
+ `escalation_ladder`, `target_demographics`, `red_flags`, `defense_summary`, `sophistication_score`.
132
+
133
+ The panel also carries a **quote check**: every quoted claim in `opening_hook` and `red_flags` is
134
+ compared against the source, and lines that are paraphrased or simply not there are marked. Use
135
+ `--strict-quotes` to make a missing line fail the run, and `--no-verify` to skip it.
136
+
137
+ Batch mode ranks a whole directory into one markdown report. A redacted report from the sample
138
+ corpus is at [`docs/sample-report.md`](docs/sample-report.md).
139
+
140
+ ## Commands
141
+
142
+ | Command | Purpose |
143
+ | --- | --- |
144
+ | `analyze --file` | Break down one script |
145
+ | `analyze --dir --batch` | Directory, aggregated report |
146
+ | `batch <dir>` | Same as above, ranked |
147
+ | `report --run-id` | Regenerate a report from a past run's log |
148
+ | `history` | List previous runs |
149
+ | `prune` | Delete old logs |
150
+
151
+ Useful flags: `--dry-run`, `--json`, `--redact`, `--top N`, `--recursive`, `--timeout`,
152
+ `--retries`, `--max-tokens`, `--no-color`. `--json` puts one object per script on stdout and all
153
+ Rich output on stderr, so it pipes:
154
+
155
+ ```bash
156
+ scamscript --json batch examples | jq -s 'map(select(.event == "result")) | length'
157
+ ```
158
+
159
+ ## How it works
160
+
161
+ ```text
162
+ script ──▶ ingest (type/size guards, optional redaction)
163
+ ──▶ analyst prompt ──▶ /chat/completions
164
+ ──▶ JSON extraction ──▶ Pydantic schema
165
+ │ invalid / truncated
166
+ └──▶ one repair retry with the error fed back
167
+ ──▶ Rich panel | JSONL record | ranked markdown
168
+ ```
169
+
170
+ - **Validation.** Replies are parsed tolerantly (fences, prose, string lists, `9/10` scores) and
171
+ then validated. One malformed reply earns exactly one repair attempt; a second failure is
172
+ reported, not retried forever.
173
+ - **Transport.** Retryable statuses (408, 409, 425, 429, 5xx) and empty replies back off and
174
+ retry within `--retries`; `Retry-After` is honoured. A `finish_reason: length` reply is treated
175
+ as truncation and repaired with a shorter-answer instruction.
176
+ - **Reasoning models.** If a provider returns only reasoning text with `finish_reason: length`, the
177
+ client treats it as truncation, and when a provider parks the answer in `reasoning` with an empty
178
+ `content`, that text is used as a last resort. Raise `--max-tokens` for models that think first.
179
+ - **Quote checking.** Quoted claims are normalised (case, quotes, punctuation) and matched against
180
+ the script. `close` means a paraphrase, `missing` means the line is not in the artifact.
181
+ Verification never drops a finding, it only marks it for the reader.
182
+ - **Scoring.** `sophistication_score` follows fixed anchors (1-2 one device, 5-6 staged trust and
183
+ one deadline, 9-10 rehearsed objection handling and controlled transfers) so rankings compare
184
+ across runs instead of drifting with prose.
185
+
186
+ ## Redaction and logs
187
+
188
+ `--redact` masks emails, card numbers, SSNs, phone numbers and long digit runs before the prompt
189
+ is sent, before anything is logged, and inside the returned breakdown. Counts are logged. Regex
190
+ masking is best effort: names, case IDs and spelled-out digits survive it.
191
+
192
+ Every request appends to a timestamped JSONL file under `~/.scamscript/logs/` (override with
193
+ `--home` or `SCAMSCRIPT_HOME`). The API key is never written. Logs are the source for `report`, so
194
+ a run replays without spending tokens, and `prune --keep 200 --keep-days 30` keeps the store
195
+ bounded.
196
+
197
+ ## Install and configure
198
+
199
+ Python 3.12+. `pip install scamscript` for use, `pip install -e ".[dev]"` from a clone for tests.
200
+ Release history is in [CHANGELOG.md](CHANGELOG.md).
201
+
202
+ | Setting | Flag | Env | Default |
203
+ | --- | --- | --- | --- |
204
+ | API key | `--api-key` | `API_KEY` | none, required unless `--dry-run` |
205
+ | Base URL | `--base-url` | `SCAMSCRIPT_BASE_URL` | `https://api.venice.ai/api/v1` |
206
+ | Model | `--model` | `SCAMSCRIPT_MODEL` | `deepseek-v4-1-flash-1017-uncensored` |
207
+ | State dir | `--home` | `SCAMSCRIPT_HOME` | `~/.scamscript` |
208
+
209
+ ## Tests, CI and calibration
210
+
211
+ ```bash
212
+ pytest
213
+ ```
214
+
215
+ The suite mocks the HTTP layer with `httpx.MockTransport`; no network or key needed. CI runs the
216
+ suite and a wheel build on Python 3.12 and 3.13.
217
+
218
+ Calibration is evidence, not vibes. `scripts/calibrate.py` measures first-attempt schema
219
+ adherence, repair rate, latency, score spread and billed cost against a live endpoint:
220
+
221
+ ```bash
222
+ API_KEY=... python scripts/calibrate.py --dir examples --repeat 3 \
223
+ --model deepseek/deepseek-v4.1-flash-1017 \
224
+ --base-url https://openrouter.ai/api/v1
225
+ ```
226
+
227
+ Recorded passes with their numbers are in [`docs/calibration.md`](docs/calibration.md): three rubric
228
+ repeats, a three-model comparison, an adversarial set and end-case stability runs. The current rubric
229
+ reaches both ends of the scale, and quote checking flags the invented lines a plain reading would
230
+ take as evidence.
231
+
232
+ The adversarial set lives in `examples/adversarial/` and has its own harness:
233
+
234
+ ```bash
235
+ API_KEY=... python scripts/injection_eval.py --model ... --base-url ...
236
+ ```
237
+
238
+ Format held in 7/7 cases; embedded demands were obeyed in 1 of 6. The fence protects the schema, not
239
+ the classification.
240
+
241
+ ## Layout
242
+
243
+ ```text
244
+ src/scamscript/
245
+ cli.py command wiring, exit codes
246
+ client.py OpenAI-compatible client, retries, truncation
247
+ analysis.py per-script loop, one repair retry
248
+ ingest.py file and directory reading with guards
249
+ models.py Pydantic schema, tolerant JSON extraction
250
+ prompts.py analyst prompt and score rubric
251
+ redact.py identifier masking
252
+ render.py Rich panels and summary
253
+ report.py ranked markdown aggregation
254
+ store.py JSONL log, run discovery, retention
255
+ verify.py quote checking against the source
256
+ tests/ schema, retry, redaction, verification, client and CLI tests
257
+ examples/ six fictional transcripts, plus adversarial/ fixtures
258
+ scripts/ calibration and injection harnesses
259
+ docs/ calibration record, sample report, banner, demo
260
+ ```
261
+
262
+ ## Limits
263
+
264
+ - Breakdowns are model readings; a bad prompt or a weak model produces confident nonsense. Read
265
+ `opening_hook` and `red_flags` against the source before quoting anything.
266
+ - Redaction is regex-based and will miss names and identifiers without a fixed shape.
267
+ - One script per request; no cross-script deduplication yet, so reprints inflate a ranking.
268
+ - Analysis is English-centric: the prompt and schema assume English artifacts.
269
+ - Quote checking compares text, not meaning. A legitimate paraphrase left in quotes is flagged;
270
+ an accurate paraphrase without quotes is skipped rather than judged.
271
+
272
+ ## Scope and disclaimer
273
+
274
+ For security research and fraud-awareness training only. Not legal advice, not an attribution
275
+ tool, no warranty. Keep API keys out of commits; the key is read from the environment.
276
+
277
+ MIT licensed, see [LICENSE](LICENSE). Contribution notes in [CONTRIBUTING.md](CONTRIBUTING.md),
278
+ vulnerability reports in [SECURITY.md](.github/SECURITY.md).
@@ -0,0 +1,19 @@
1
+ scamscript/__init__.py,sha256=ve6tW4xYXG0aWgQdTqxFMv7YyUwjxn3_PoLfAFQjpJg,93
2
+ scamscript/__main__.py,sha256=1FkTauyNgx-VTivcgblCC--ehR94Le1jOwKbqsaPtNI,69
3
+ scamscript/analysis.py,sha256=PzHgxwdWyuXMltRyPO8bu0B7Ps0GQn8axlG7otCPxis,4483
4
+ scamscript/cli.py,sha256=wwr0iPTmPvrjkx53tnti03vfR3X5HkAnaymGoYIH4VA,16871
5
+ scamscript/client.py,sha256=IXoCpTaI0dY56E_IUlWHKgC0ncF97-ZQ5owo8n7c1yo,6734
6
+ scamscript/config.py,sha256=Zp1dzQMhR7T4lM-rgv9n-CrGSI8TT2MCOOcw8JunGSM,1628
7
+ scamscript/ingest.py,sha256=UK0dKuz0DiIVNYRlqOXrsppdXfD2_SB6aQ2xa9XtdYM,2639
8
+ scamscript/models.py,sha256=XvEeRrOdTK-94Qf9kylcumTCtlK5sLpuslLRI4k5f50,4935
9
+ scamscript/prompts.py,sha256=OjvdE--4LLi-KGgt5ofctBVfAjSBIOS_-sVpmeG7vMo,4896
10
+ scamscript/redact.py,sha256=XRtoyICpvHa_ShutSY7xPI-CA2ynwl1mhMrKG5Ku-J4,1885
11
+ scamscript/render.py,sha256=77h2SAyfxuVkdQ2JeNZTpmahevUh4emNEKGsiVbUOLo,7309
12
+ scamscript/report.py,sha256=7qPAoM2ySmH6fP7gov4Xd7tDNpDerl2n_u1tLzHYulE,6970
13
+ scamscript/store.py,sha256=ivqpkK9-4ZLjpwhTfbyfKdUi3SLQRLcdHz3raJcDDuA,5694
14
+ scamscript/verify.py,sha256=r-LAsixI0VBhS8B1kysGpOH25gFNxTOx8YWShqKTrSY,3136
15
+ scamscript-0.2.0.dist-info/METADATA,sha256=O9tXoKOAsJ27dWVxsX-SNY0tMmUAFMlP686KB6uySUc,13564
16
+ scamscript-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
17
+ scamscript-0.2.0.dist-info/entry_points.txt,sha256=0_9QvsVOPGBfHyKJeIuMRipxlZ93RkSzBi0BbuVxRFw,50
18
+ scamscript-0.2.0.dist-info/licenses/LICENSE,sha256=igeAjcsoQ1OuxBrqTyijwNZ9mcsYmJLkLfn7M5rmGg4,1080
19
+ scamscript-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scamscript = scamscript.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 scamscript contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.