Metadata-Version: 2.5
Name: no-phone-home
Version: 0.1.0
Summary: Privacy-focused AST scanner that traces user/system data to network sinks and flags telemetry that ignores opt-out.
Project-URL: Homepage, https://github.com/mkamranr/no-phone-home
Project-URL: Issues, https://github.com/mkamranr/no-phone-home/issues
Author: No Phone Home Contributors
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: privacy,sarif,static-analysis,taint-analysis,telemetry,tree-sitter
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Requires-Dist: pathspec>=0.12
Requires-Dist: pydantic>=2.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=13.0
Requires-Dist: tree-sitter-go>=0.25
Requires-Dist: tree-sitter-javascript>=0.25
Requires-Dist: tree-sitter-python>=0.25
Requires-Dist: tree-sitter-typescript>=0.23
Requires-Dist: tree-sitter<0.27,>=0.25
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: jsonschema>=4.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

<div align="center">

# 🛡️ No Phone Home

### **Find the telemetry that doesn't ask permission.**

*A privacy-focused static analyzer that traces your machine's data to the network — and flags every path that never checks whether you said no.*

[![CI](https://github.com/mkamranr/no-phone-home/actions/workflows/ci.yml/badge.svg)](https://github.com/mkamranr/no-phone-home/actions/workflows/ci.yml)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-green.svg)](LICENSE)
[![SARIF 2.1.0](https://img.shields.io/badge/SARIF-2.1.0-informational.svg)](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html)
[![Languages](https://img.shields.io/badge/languages-Python%20%7C%20JS%2FTS%20%7C%20Go-orange.svg)](#-language-support)

[Install](#-installation) · [Quick start](#-quick-start) · [CI setup](#-continuous-integration) · [Custom rules](docs/rules.md) · [How it works](docs/architecture.md)

</div>

---

## The problem

Traditional scanners — Semgrep, Bandit, CodeQL — hunt CVEs and OWASP classes. Silent telemetry isn't a vulnerability in that sense, so it walks straight through every existing gate.

**No Phone Home** asks the question a privacy auditor actually asks:

> *Does this code take data about my machine and my identity, ship it over the network, and never check whether I said no?*

It answers by running **source-to-sink taint analysis** over tree-sitter ASTs, then verifying whether each network egress is genuinely gated by a consent check such as `DO_NOT_TRACK`.

---

## 🔍 What it catches

### 1. Unguarded exfiltration

```python
import socket, requests

host = socket.gethostname()                              # 1. tainted source
payload = {"device": host}                               # 2. taint propagates
requests.post("https://telemetry.dev/api", json=payload) # 3. egress, no guard  ❌
```

### 2. Inverted guards — the case most tools get backwards

```python
if os.getenv("DO_NOT_TRACK"):
    requests.post(url, json=payload)   # ⛔ transmits *because* you opted out
```

A naive *"is there an enclosing `if` mentioning DO_NOT_TRACK?"* check calls this **safe**. No Phone Home reasons about **polarity** — which way the consent token points, and whether it is truthy or falsy where the sink actually runs — so it reports this at HIGH severity as a distinct `INVERTED_GUARD` verdict.

The same logic works across languages. In Go:

```go
host, _ := os.Hostname()
if os.Getenv("DO_NOT_TRACK") != "" {
    http.Post(url, "application/json", host)  // ⛔ sends only when DNT *is* set
}
```

### 3. Guard clauses — how real code is actually written

```python
def report():
    if os.getenv("DO_NOT_TRACK"):
        return                                   # ✅ dominating guard, correctly cleared
    requests.post(url, json={"host": socket.gethostname()})
```

An ancestor-only `if` walk misses this entirely, because a guard clause is not an enclosing conditional.

### 4. Telemetry hidden one call deep

```python
def _send(payload):
    requests.post(URL, json=payload)             # sink lives here

def track():
    _send({"user": getpass.getuser()})           # ❌ still caught, via function summaries
```

This is how telemetry SDKs are actually structured, and it is invisible to a purely intraprocedural pass.

---

## 📦 Installation

Requires **Python 3.10+**. No other runtime — the tree-sitter grammars ship as prebuilt wheels.

**From GitHub** (works today):

```bash
pip install git+https://github.com/mkamranr/no-phone-home.git
```

**From source** (for development):

```bash
git clone https://github.com/mkamranr/no-phone-home.git
cd no-phone-home
pip install -e ".[dev]"
pytest          # 67 tests
```

Using [`uv`](https://github.com/astral-sh/uv):

```bash
uv venv && uv pip install -e ".[dev]"
uv run no-phone-home ./src
```

> **Note** · A PyPI release (`pip install no-phone-home`) is planned — see the [release workflow](.github/workflows/release.yml). Until then, install from GitHub as above.

---

## 🚀 Quick start

```bash
no-phone-home ./src                      # scan a directory
no-phone-home app.py                     # scan one file
no-phone-home . --format markdown        # Privacy Nutrition Label
no-phone-home . --list-rules             # see active rules
```

Typical output:

```
┏━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Sev     ┃ Rule   ┃ Location          ┃ Private data → destination   ┃ Guard      ┃
┡━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ HIGH    │ ST-001 │ metrics.py:34     │ socket.gethostname() →       │ UNGUARDED  │
│         │        │                   │ https://telemetry.dev/api    │            │
│ HIGH    │ ST-002 │ analytics.py:12   │ getpass.getuser() →          │ INVERTED   │
│         │        │                   │ https://api.mixpanel.com     │            │
└─────────┴────────┴───────────────────┴──────────────────────────────┴────────────┘
23 files scanned · 8 taint paths · 5 privacy violations
⛔ 1 inverted guard(s): data is sent on the branch where the user opted OUT.
```

---

## 📋 Output formats

| Format | Flag | Use it for |
| :--- | :--- | :--- |
| **Table** | `--format table` *(default)* | Reading in a terminal |
| **Privacy Nutrition Label** | `--format markdown` | PR comments, READMEs, audit reports |
| **SARIF 2.1.0** | `--format sarif` | GitHub code scanning — taint traces render as clickable code flows |
| **JSON** | `--format json` | Scripting and dashboards |

The Privacy Nutrition Label gives non-engineers a readable summary:

```markdown
## 📋 Privacy Nutrition Label
| Data Category | Detected Source | Egress Destination | Guard Status |
| :--- | :--- | :--- | :--- |
| **System Info** | `platform.node()` | `https://telemetry.dev/api` | ❌ **UNGUARDED** |
| **User Identity** | `os.userInfo()` | `https://api.mixpanel.com` | ✅ Guarded (`DO_NOT_TRACK`) |
| **Environment** | `process.env` | Local Log File | ℹ️ Internal Only |
```

---

## ⚙️ Continuous integration

### GitHub Actions

No Phone Home ships as a composite action. SARIF uploads land in your repo's **Security** tab:

```yaml
name: Privacy scan
on: [push, pull_request]

permissions:
  contents: read
  security-events: write

jobs:
  no-phone-home:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: mkamranr/no-phone-home@v0.1.0
        with:
          path: ./src
          fail-on: HIGH
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: no-phone-home.sarif
```

Or call the CLI directly:

```yaml
- run: pip install git+https://github.com/mkamranr/no-phone-home.git
- run: no-phone-home ./src --format sarif --output results.sarif --fail-on HIGH
```

### Exit codes

| Code | Meaning |
| :--- | :--- |
| `0` | Clean — no violations at or above the threshold |
| `1` | Violations found at or above `--fail-on` |
| `2` | Execution error (bad path, unreadable rules file) |

Distinguishing `1` from `2` is what makes it safe to gate a build on.

### pre-commit

```yaml
repos:
  - repo: https://github.com/mkamranr/no-phone-home
    rev: v0.1.0
    hooks:
      - id: no-phone-home
```

---

## 🎛️ CLI reference

```
no-phone-home [PATH] [OPTIONS]
```

| Option | Default | Description |
| :--- | :--- | :--- |
| `PATH` | `.` | File or directory to scan |
| `-f, --format` | `table` | `table`, `json`, `markdown`, `sarif` |
| `-o, --output` | stdout | Write the report to a file |
| `--fail-on` | `HIGH` | Exit 1 at/above this severity, or `never` |
| `--min-confidence` | `LOW` | Drop findings below `HIGH` / `MEDIUM` / `LOW` |
| `--include-deps` | off | Also scan `node_modules/`, `.venv/`, `vendor/` |
| `--rules` | auto | Path to a `.nph-rules.yml` |
| `--baseline` | none | Ignore findings recorded in this file |
| `--write-baseline` | — | Snapshot current findings, then exit 0 |
| `--no-gitignore` | off | Don't honour `.gitignore` |
| `--exclude` | — | Extra directory name to skip (repeatable) |
| `--list-rules` | — | Print active rules and exit |
| `--version` | — | Print version and exit |

Full details in **[docs/usage.md](docs/usage.md)**.

---

## 📐 Custom rules

Rules are declarative YAML — extending coverage needs **no Python**. Drop a `.nph-rules.yml` in your repo root:

```yaml
version: "1.0"

rules:
  - id: ST-001
    name: Unchecked Hostname Exfiltration
    severity: HIGH
    language: python
    category: SYSTEM_INFO
    sources:
      - pattern: "socket.gethostname()"
      - pattern: "platform.node()"
    sinks:
      - pattern: "requests.post($URL, data=$DATA)"
    guards:
      - pattern: "os.getenv('DO_NOT_TRACK')"
    remediation: "Wrap the request in an explicit opt-out check."
```

Patterns are parsed with the **same tree-sitter grammar as your code**, so they're real syntax rather than regexes. `$NAME` is a metavariable wildcard.

📖 **[Full rule-authoring guide →](docs/rules.md)**

---

## 🔇 Suppression

Inline, scoped to a specific rule:

```python
requests.post(url, json=payload)  # no-phone-home: ignore[ST-001]
```

Or snapshot an existing codebase and fail only on *new* findings:

```bash
no-phone-home . --write-baseline .nph-baseline.json   # once
no-phone-home . --baseline .nph-baseline.json         # in CI
```

---

## 🌐 Language support

All three languages run the **same** engine — taint propagation, same-file function summaries, and polarity-aware guard verification. Only rule coverage differs.

| Language | Extensions | Engine | Builtin rules |
| :--- | :--- | :--- | :--- |
| **Python** | `.py` `.pyi` | ✅ Full | 6 |
| **JavaScript / TypeScript** | `.js` `.mjs` `.cjs` `.jsx` `.ts` `.tsx` `.mts` `.cts` | ✅ Full | 4 |
| **Go** | `.go` | ✅ Full | 4 |

Adding a language means implementing one adapter protocol — the analyzer never names a tree-sitter node type directly. See [docs/architecture.md](docs/architecture.md).

---

## 🧠 How it works

```
[ Source Code ]
      │
      ▼
[ Parser (tree-sitter) ] ──► Language ASTs (Python, JS/TS, Go)
      │
      ▼
[ Normalized IR ]
      │
      ▼
[ Taint Engine ]
      ├── Source matching      (system / user / hardware APIs)
      ├── Propagation          (assignment, dicts, f-strings, concat)
      ├── Function summaries   (same-file, cross-function)
      └── Sink matching        (HTTP clients / telemetry SDKs)
      │
      ▼
[ Guard Evaluator ]  ──► GUARDED / UNGUARDED / INVERTED_GUARD / LOCAL_ONLY
      │
      ▼
[ Reporters ] ──► Table · Nutrition Label · JSON · SARIF
```

📖 **[Architecture deep dive →](docs/architecture.md)**

---

## ⚠️ Honest limitations

**This is a heuristic static analyzer, not a proof of absence.**

It reads code without running it, so it will miss dynamically-constructed exfiltration (`getattr`, `eval`, reflection, runtime-assembled URLs), and it will sometimes flag benign code. **A clean scan is not a certificate of privacy.**

Its value is making silent telemetry **visible and reviewable** — not certifying innocence. Every finding carries a confidence level precisely so you can tell "definitely" from "worth a look".

Scope boundaries:

- **First-party code only by default.** Vendor directories are skipped; `--include-deps` opts in, at the cost of slower scans and findings you can't directly fix.
- **Intraprocedural + same-file summaries.** Taint crossing module boundaries is not tracked; a whole-program call graph is where scanners lose accuracy and speed.
- Hashing a value **lowers confidence but does not clear taint** — a hashed hardware ID is still a stable identifier.

---

## 📚 Documentation

| Document | Contents |
| :--- | :--- |
| **[docs/usage.md](docs/usage.md)** | Complete CLI reference, workflows, CI recipes, troubleshooting |
| **[docs/rules.md](docs/rules.md)** | Writing custom rules, pattern syntax, guard vocabulary |
| **[docs/architecture.md](docs/architecture.md)** | How the engine works, adding a language |
| **[CONTRIBUTING.md](CONTRIBUTING.md)** | Development setup, fixture requirements |
| **[SECURITY.md](SECURITY.md)** | Reporting vulnerabilities, threat model |
| **[CHANGELOG.md](CHANGELOG.md)** | Release history |

---

## 🤝 Contributing

The highest-value contribution is **rules and fixtures** — both are YAML and plain sample code, no engine knowledge needed.

Every detection change ships with fixtures in both directions: a `true_positive/` case *and* the correctly-guarded `true_negative/` form. The corpus harness enforces **100% precision and recall**, which is deliberate — for a privacy scanner a false positive costs more than a miss, because it's what gets the tool deleted from CI.

See **[CONTRIBUTING.md](CONTRIBUTING.md)**.

---

## 📄 License

[Apache-2.0](LICENSE)

<div align="center">
<sub>Built for developers and auditors who'd rather know what their dependencies are sending home.</sub>
</div>
