Metadata-Version: 2.5
Name: scamscript
Version: 0.8.0
Summary: Reverse-engineer scam scripts and vishing transcripts into structured counter-playbooks
Project-URL: Homepage, https://github.com/sjdsjadjajdjsa/scamscript
Project-URL: Repository, https://github.com/sjdsjadjajdjsa/scamscript
Project-URL: Issues, https://github.com/sjdsjadjajdjsa/scamscript/issues
Project-URL: Changelog, https://github.com/sjdsjadjajdjsa/scamscript/blob/master/CHANGELOG.md
Author-email: scamscript contributors <313729361+sjdsjadjajdjsa@users.noreply.github.com>
Maintainer-email: scamscript contributors <313729361+sjdsjadjajdjsa@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Keywords: cli,fraud,osint,phishing,scam,security,smishing,social-engineering,transcripts,vishing
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: httpx==0.28.1
Requires-Dist: pydantic==2.14.2
Requires-Dist: rich==15.0.0
Requires-Dist: typer==0.27.3
Provides-Extra: dev
Requires-Dist: build==1.3.0; extra == 'dev'
Requires-Dist: pytest==8.4.1; extra == 'dev'
Requires-Dist: twine==6.1.0; extra == 'dev'
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/sjdsjadjajdjsa/scamscript/master/docs/banner.svg" alt="scamscript: reverse-engineer the playbook, not the story" width="100%">
</p>

<p align="center">
  <img src="https://raw.githubusercontent.com/sjdsjadjajdjsa/scamscript/master/docs/demo.gif" alt="terminal demo: analyzing a script, then ranking a directory" width="760">
  <br>
  <sub>Recorded against a local stub endpoint, so no API spend.</sub>
</p>

# scamscript

[![CI](https://github.com/sjdsjadjajdjsa/scamscript/actions/workflows/ci.yml/badge.svg)](https://github.com/sjdsjadjajdjsa/scamscript/actions/workflows/ci.yml)
[![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13-3776ab)](pyproject.toml)
[![License](https://img.shields.io/badge/license-MIT-2a9d8f)](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/LICENSE)
[![Scope](https://img.shields.io/badge/scope-security%20research-e63946)](#scope-and-disclaimer)

A reverse-engineering analyzer for leaked or public scam call scripts and vishing transcripts.
Point it at a script and a model takes the fraud apart: the persona, the hook that catches you,
how trust is manufactured, the pressure applied, what happens when you hesitate, and how to
counter that exact playbook.

> **Security research and fraud-awareness training only.** Output is a model-produced reading of
> a captured script, not legal advice and not attribution. A script is evidence of a *pattern*;
> the live scam that reuses it will differ in details.

---

## Why

Scripts leak. Call centres dump their flows, victims publish transcripts, researchers collect
samples. Reading them one by one teaches you that `your account is at risk` is bad, but not how a
particular operation sequences trust into a transfer. This tool turns each artifact into a
comparable record, so a corpus can be ranked and a defender's briefing can quote the actual
mechanics instead of general advice.

## Quickstart

```bash
pip install scamscript           # or, for development:
# git clone https://github.com/sjdsjadjajdjsa/scamscript && cd scamscript && pip install -e ".[dev]"

export API_KEY=...
export SCAMSCRIPT_BASE_URL=https://openrouter.ai/api/v1
export SCAMSCRIPT_MODEL=deepseek/deepseek-v4.1-flash-1017

scamscript --dry-run analyze --file examples/tech_support_scam.txt   # inspect the prompt, spend nothing
scamscript analyze --file examples/tech_support_scam.txt
```

Any OpenAI-compatible `/chat/completions` endpoint works. There is no baked-in provider or model:
pass `--base-url` and `--model`, or set `SCAMSCRIPT_BASE_URL` and `SCAMSCRIPT_MODEL`.

```bash
scamscript --base-url https://openrouter.ai/api/v1 \
  --model deepseek/deepseek-v4.1-flash-1017 analyze --file script.txt

scamscript --base-url http://localhost:11434/v1 --model llama3.2 analyze --file script.txt
```

## What it produces

Each script renders as a colour-coded panel:

```text
╭──────────────────────────── tech_support_scam.txt ────────────────────────────╮
│ ATTACK TYPE   TECH SUPPORT SCAM     SOPHISTICATION  6/10                     │
│                                                                              │
│ OPENING HOOK                                                                 │
│   "Our telemetry shows your machine has been broadcasting error ..."         │
│                                                                              │
│ RED FLAGS                                                                    │
│   ✗ Unsolicited call claiming Windows telemetry                              │
│   ✗ Caller asks for a Remote Desktop session ID                              │
╰────────────────────────────────── 7 red flags ───────────────────────────────╯
```

Fields: `attack_type`, `channel`, `persona`, `opening_hook`, `trust_building_moves`, `urgency_tactics`,
`escalation_ladder`, `target_demographics`, `red_flags`, `defense_summary`, `sophistication_score`.

The panel also carries a **quote check**: every quoted claim in `opening_hook` and `red_flags` is
compared against the source, and lines that are paraphrased or simply not there are marked. Use
`--strict-quotes` to make a missing line fail the run, and `--no-verify` to skip it.

Batch mode ranks a whole directory into one markdown report. A redacted report from the sample
corpus is at [`docs/sample-report.md`](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/docs/sample-report.md).

## Commands

| Command | Purpose |
| --- | --- |
| `analyze --file` | Break down one script |
| `analyze --dir --batch` | Directory, aggregated report |
| `batch <dir>` | Same as above, ranked |
| `report --run-id` | Regenerate a report from a past run's log |
| `history` | List previous runs |
| `brief` | One-page defence handout from a finished run |
| `prune` | Delete old logs |

Useful flags: `--dry-run`, `--json`, `--redact`, `--top N`, `--recursive`, `--timeout`,
`--retries`, `--max-tokens`, `--concurrency`, `--max-cost`, `--dedupe/--no-dedupe`,
`--show-duplicates`, `--type`, `--channel`, `--language`, `--force`, `--no-color`. `--json` puts one object per script on stdout and all
Rich output on stderr, so it pipes:

```bash
scamscript --json batch examples | jq -s 'map(select(.event == "result")) | length'
```

## Scale and spend

`--concurrency N` runs up to 16 analyses at once and keeps output in script order. `--max-cost`
stops scheduling new scripts once the provider reports that much spend, marks the remainder skipped
and still exits zero when nothing failed.

```bash
scamscript --concurrency 6 --max-cost 0.25 batch corpus/ --output report.md
```

The cap reads `usage.cost` from the response, so it works only where the provider reports cost and
says so when none is sent. Spend is printed and logged per run. In-flight requests can overshoot by
up to the concurrency, so keep headroom on a hard limit.

## Real corpus

`corpus/real/` holds ten published transcripts with source, date and verbatim status in every
header: a 40-minute bank impersonation call, a Medicare benefits pitch, six FTC robocalls, a
recalled credit-union call and a USPS smishing message. Provenance and the results of running them
are in [`docs/real-corpus.md`](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/docs/real-corpus.md).

```bash
scamscript --redact batch corpus/real --concurrency 3 --max-cost 0.25 --output real.md
```

## Attack types

`attack_type` is free prose, which is useful to read and useless to group. Every result also carries a
closed-set slug (`vishing`, `smishing`, `tech-support`, `refund`, `gov-impersonation`, `bec`,
`romance`, `crypto`, `investment`, `utility`, `delivery`, `other`). Reports show counts and mean scores
per slug above the ranking, and `--type` filters to the ones you ask for:

```bash
scamscript batch corpus/ --type tech-support,refund --output report.md
```

Delivery is tracked separately from theme. `channel` is one of `vishing`, `smishing`, `email` or
`other`, so a delivery-themed text lure is `delivery` + `smishing` rather than losing one of the two.
`--channel` filters on it:

```bash
scamscript batch corpus/real --channel smishing --output texts.md
```

Briefings group by type but accept `--channel` too. Build one from a finished run:

```bash
scamscript brief --type tech-support --output briefing.md
```

The raw phrase is always kept next to the slug. An unrecognised description lands in `other` and the
CLI names any filter token it did not recognise.

## Duplicates

Corpora are full of reprints. Each script is fingerprinted on its body: headers, casing, punctuation
and whitespace are ignored, so copies collapse. The strongest copy is ranked and the rest are listed
under it; the report counts what was folded and JSON records carry `fingerprint` and `duplicate_of`.

```bash
scamscript batch corpus/ --output report.md          # folds by default
scamscript batch corpus/ --no-dedupe --show-duplicates
```

Only byte-level twins fold. A copy with one changed account number stays separate, by design.

## How it works

```text
script ──▶ ingest (type/size guards, optional redaction)
        ──▶ analyst prompt ──▶ /chat/completions
        ──▶ JSON extraction ──▶ Pydantic schema
                 │ invalid / truncated
                 └──▶ one repair retry with the error fed back
        ──▶ Rich panel  |  JSONL record  |  ranked markdown
```

- **Validation.** Replies are parsed tolerantly (fences, prose, string lists, `9/10` scores) and
  then validated. One malformed reply earns exactly one repair attempt; a second failure is
  reported, not retried forever.
- **Transport.** Retryable statuses (408, 409, 425, 429, 5xx) and empty replies back off and
  retry within `--retries`; `Retry-After` is honoured. A `finish_reason: length` reply is treated
  as truncation and repaired with a shorter-answer instruction.
- **Reasoning models.** If a provider returns only reasoning text with `finish_reason: length`, the
  client treats it as truncation, and when a provider parks the answer in `reasoning` with an empty
  `content`, that text is used as a last resort. Raise `--max-tokens` for models that think first.
- **Quote checking.** Quoted claims are normalised (case, quotes, punctuation) and matched against
  the script. `close` means a paraphrase, `missing` means the line is not in the artifact.
  Verification never drops a finding, it only marks it for the reader.
- **Scoring.** `sophistication_score` follows fixed anchors (1-2 one device, 5-6 staged trust and
  one deadline, 9-10 rehearsed objection handling and controlled transfers) so rankings compare
  across runs instead of drifting with prose.

## Redaction and logs

`--redact` masks emails, card numbers, SSNs, phone numbers and long digit runs before the prompt
is sent, before anything is logged, and inside the returned breakdown. Counts are logged. Regex
masking is best effort: names, case IDs and spelled-out digits survive it.

Every request appends to a timestamped JSONL file under `~/.scamscript/logs/` (override with
`--home` or `SCAMSCRIPT_HOME`). The API key is never written. Logs are the source for `report`, so
a run replays without spending tokens, and `prune --keep 200 --keep-days 30` keeps the store
bounded. Directories are `0700` and log files `0600`.

## Printable briefings

`brief --html` writes a self-contained page with inline CSS and escaped text, for handing to people
who will not open a terminal.

```bash
scamscript brief --type tech-support --html --output handout.html
```

## Paths

`--file` and `--dir` resolve symlinks, a recursive walk survives a symlink loop, and a file that
changes while it is read is an error rather than a half-read prompt. Output files are not overwritten
unless `--force` is passed.

## Install and configure

Python 3.12+. `pip install scamscript` for use, `pip install -e ".[dev]"` from a clone for tests.
Release history is in [CHANGELOG.md](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/CHANGELOG.md).

| Setting | Flag | Env | Default |
| --- | --- | --- | --- |
| API key | `--api-key` | `API_KEY` | none, required unless `--dry-run` |
| Base URL | `--base-url` | `SCAMSCRIPT_BASE_URL` | none, required |
| Model | `--model` | `SCAMSCRIPT_MODEL` | none, required |
| State dir | `--home` | `SCAMSCRIPT_HOME` | `~/.scamscript` |

## Tests, CI and calibration

```bash
pytest
```

The suite mocks the HTTP layer with `httpx.MockTransport`; no network or key needed. CI runs the
suite and a wheel build on Python 3.12 and 3.13.

Calibration is evidence, not vibes. `scripts/calibrate.py` measures first-attempt schema
adherence, repair rate, latency, score spread and billed cost against a live endpoint:

```bash
API_KEY=... SCAMSCRIPT_BASE_URL=https://openrouter.ai/api/v1 \
  python scripts/calibrate.py --dir examples --repeat 3 \
  --model deepseek/deepseek-v4.1-flash-1017
```

Recorded passes with their numbers are in [`docs/calibration.md`](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/docs/calibration.md): three rubric
repeats, a three-model comparison, an adversarial set and end-case stability runs. The current rubric
reaches both ends of the scale, and quote checking flags the invented lines a plain reading would
take as evidence.

The adversarial set lives in `examples/adversarial/` and has its own harness:

```bash
API_KEY=... SCAMSCRIPT_BASE_URL=... python scripts/injection_eval.py --model ...
```

Format held in 7/7 cases; embedded demands were obeyed in 1 of 6. The fence protects the schema, not
the classification.

## Layout

```text
src/scamscript/
  cli.py        command wiring, exit codes
  client.py     OpenAI-compatible client, retries, truncation
  analysis.py   per-script loop, one repair retry
  ingest.py     file and directory reading with guards
  models.py     Pydantic schema, tolerant JSON extraction
  prompts.py    analyst prompt and score rubric
  redact.py     identifier masking
  render.py     Rich panels and summary
  report.py     ranked markdown aggregation
  store.py      JSONL log, run discovery, retention
  verify.py     quote checking against the source
tests/          schema, retry, redaction, verification, client and CLI tests
examples/       six fictional transcripts, plus adversarial/ fixtures
corpus/real/    ten published transcripts with provenance
scripts/        calibration and injection harnesses
docs/           calibration record, sample report, banner, demo
```

## Limits

- Breakdowns are model readings; a bad prompt or a weak model produces confident nonsense. Read
  `opening_hook` and `red_flags` against the source before quoting anything.
- Redaction is regex-based and will miss names and identifiers without a fixed shape.
- Duplicate folding ignores formatting only; a single changed digit keeps a copy separate.
- Type slugs are keyword-matched, so an unusual description falls to `other` rather than inventing one.
- The cost cap depends on provider-reported usage and can overshoot by the in-flight count.
- `channel` is inferred from the model's answer or, failing that, from keywords; a script with no
  channel words falls to `other`.
- The prompt asks the model to answer in the script's own language and `--language` overrides
  detection, but the type keywords are English: a non-English script often falls to `other`.
- Quote checking compares text, not meaning. A legitimate paraphrase left in quotes is flagged;
  an accurate paraphrase without quotes is skipped rather than judged.

## Scope and disclaimer

For security research and fraud-awareness training only. Not legal advice, not an attribution
tool, no warranty. Keep API keys out of commits; the key is read from the environment.

MIT licensed, see [LICENSE](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/LICENSE). Contribution notes in [CONTRIBUTING.md](https://github.com/sjdsjadjajdjsa/scamscript/blob/master/CONTRIBUTING.md),
vulnerability reports in [SECURITY.md](.github/SECURITY.md).
