Metadata-Version: 2.4
Name: folija
Version: 1.0.0
Summary: Runtime Integrity Membrane for Python — know when your code changes
Author-email: OpenDOK Foundation <hello@opendok.org>
License: MIT
Project-URL: Homepage, https://github.com/opendok/folija
Project-URL: Repository, https://github.com/opendok/folija
Project-URL: Issues, https://github.com/opendok/folija/issues
Project-URL: Changelog, https://github.com/opendok/folija/blob/main/CHANGELOG.md
Keywords: security,integrity,runtime,tamper-detection,hash,monitoring,supply-chain
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: django
Requires-Dist: django>=3.2; extra == "django"
Provides-Extra: flask
Requires-Dist: flask>=2.0; extra == "flask"
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100; extra == "fastapi"
Requires-Dist: pydantic>=2.0; extra == "fastapi"
Provides-Extra: api
Requires-Dist: fastapi>=0.100; extra == "api"
Requires-Dist: pydantic>=2.0; extra == "api"
Requires-Dist: uvicorn[standard]>=0.20; extra == "api"
Provides-Extra: all
Requires-Dist: django>=3.2; extra == "all"
Requires-Dist: flask>=2.0; extra == "all"
Requires-Dist: fastapi>=0.100; extra == "all"
Requires-Dist: pydantic>=2.0; extra == "all"
Requires-Dist: uvicorn[standard]>=0.20; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: httpx; extra == "dev"
Dynamic: license-file

# Folija

**Runtime Integrity Membrane for Python**

> *"Your code is either what you deployed, or it isn't.  
> Folija makes sure you always know which."*

[![PyPI](https://img.shields.io/pypi/v/folija)](https://pypi.org/project/folija/)
[![Python](https://img.shields.io/pypi/pyversions/folija)](https://pypi.org/project/folija/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Zero dependencies](https://img.shields.io/badge/dependencies-zero-brightgreen)](pyproject.toml)
[![Downloads](https://img.shields.io/pypi/dm/folija)](https://pypi.org/project/folija/)

---

## The Problem

You deploy your application. Everything is green.  
Then, somewhere between your CI pipeline and production, something changes.

A file is modified. A monkey-patch is injected. A dependency is swapped.  
Your code does something different — and you have no idea.

**Supply chain attacks don't ring a bell. They whisper.**

---

## What Folija Does

Folija is a **zero-dependency Python library** that answers one question at all times:

> *Is the code running right now exactly the code I deployed?*

It works by:

1. **Baselining** your deployed files — computing SHA-256 of every `.py` (or `.so`) file you care about.
2. **Sitting silently** in `sys.meta_path` (Python's import hook chain).
3. **Checking the hash** every time Python imports a watched module.
4. **Firing your callbacks** the moment a mismatch is detected — before the tampered code runs again.

No daemon. No scheduled cron. No database. No config files.  
200 lines of pure Python. Zero mandatory dependencies.

---

## Installation

```bash
# Core — zero dependencies, works everywhere
pip install folija

# With Django middleware
pip install folija[django]

# With Flask extension
pip install folija[flask]

# With FastAPI/ASGI middleware
pip install folija[fastapi]

# With REST API server (FastAPI + uvicorn)
pip install folija[api]

# Everything
pip install folija[all]
```

---

## Real-World Scenarios — Every Combination

### Scenario 1: Simple script, one file to protect

```python
import folija

# Hash the file right now, use it as expected baseline
result = folija.verify_file("/srv/app/payments.py")
if result.status == "NO_BASELINE":
    # First run — establish trust
    from folija.baseline import create_baseline
    create_baseline(["/srv/app/payments.py"], output_path="/srv/baselines/app.json")
else:
    if not result.ok:
        raise RuntimeError(f"TAMPER: {result.path}")
```

---

### Scenario 2: Django application — startup integrity check

```python
# myapp/apps.py
from django.apps import AppConfig

class MyAppConfig(AppConfig):
    name = "myapp"

    def ready(self):
        import folija
        from folija.baseline import load_baseline
        from folija.report import IntegrityReport

        load_baseline("/srv/baselines/myapp.json")
        folija.watch_directory("/srv/app/myapp/")

        @folija.on_tamper
        def on_tamper(module, expected, actual):
            import logging
            log = logging.getLogger("security")
            log.critical(
                "TAMPER DETECTED | module=%s | expected=%.16s | actual=%.16s",
                module, expected, actual
            )
            # Send to your SIEM / PagerDuty / Slack here

        folija.activate()
        report = IntegrityReport.run()
        report.raise_if_tampered()   # Abort startup if anything is wrong
```

---

### Scenario 3: Django — per-request monitoring via middleware

```python
# settings.py
MIDDLEWARE = [
    "folija.middleware.django_middleware.FolijaMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # ... rest of your middleware
]

FOLIJA_BASELINE        = BASE_DIR / ".folija_baseline.json"
FOLIJA_WATCHED_DIRS    = [BASE_DIR / "payments", BASE_DIR / "auth"]
FOLIJA_BLOCK_ON_TAMPER = False  # True = return HTTP 503 on tamper

# Optional: custom callback
FOLIJA_CALLBACKS = []  # add callables here if needed
```

---

### Scenario 4: Flask application

```python
# app/__init__.py
from flask import Flask
from folija.middleware.flask_middleware import FolijaFlask

def create_app():
    app = Flask(__name__)

    FolijaFlask(
        app,
        baseline="/srv/baselines/myapp.json",
        watched_dirs=["app/"],
        block_on_tamper=False,
        callbacks=[lambda mod, exp, act: print(f"TAMPER: {mod}")],
    )

    return app
```

Or via `app.config`:

```python
app.config["FOLIJA_BASELINE"]        = "/srv/baselines/myapp.json"
app.config["FOLIJA_WATCHED_DIRS"]    = ["app/payments", "app/auth"]
app.config["FOLIJA_BLOCK_ON_TAMPER"] = True  # 503 if tampered

FolijaFlask(app)
```

---

### Scenario 5: FastAPI application

```python
# main.py
from fastapi import FastAPI
from folija.middleware.fastapi_middleware import FolijaMiddleware

app = FastAPI()
app.add_middleware(
    FolijaMiddleware,
    baseline="/srv/baselines/myapp.json",
    watched_dirs=["app/"],
    block_on_tamper=False,
)
```

Or with the lifespan pattern (recommended for FastAPI 0.93+):

```python
from contextlib import asynccontextmanager
import folija
from folija.baseline import load_baseline

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    load_baseline("/srv/baselines/myapp.json")
    folija.watch_directory("app/")
    folija.activate(callbacks=[my_alert_callback])

    yield  # ← application runs here

    # Shutdown
    folija.deactivate()

app = FastAPI(lifespan=lifespan)
```

---

### Scenario 6: Starlette (raw ASGI)

```python
from starlette.applications import Starlette
from starlette.routing import Route
from folija.middleware.fastapi_middleware import FolijaMiddleware

app = Starlette(routes=[...])
app.add_middleware(
    FolijaMiddleware,
    baseline="/srv/baselines/app.json",
    watched_dirs=["app/"],
)
```

---

### Scenario 7: Any framework — manual integration

```python
import folija
from folija.baseline import load_baseline

# Step 1: Load your baseline at startup
load_baseline("/srv/baselines/app.json")

# Step 2: Register modules you care about most
folija.watch("app.payments.processor")
folija.watch("app.auth.backend")
folija.watch("app.crypto.signing")

# Step 3: Or watch entire directories
folija.watch_directory("/srv/app/", pattern="*.py", recursive=True)
folija.watch_directory("/srv/app/", pattern="*.so", recursive=True)   # compiled extensions too

# Step 4: Register callbacks
@folija.on_tamper
def critical_alert(module_name, expected_hash, actual_hash):
    """Called asynchronously in a daemon thread — never blocks your app."""
    # PagerDuty
    requests.post("https://events.pagerduty.com/v2/enqueue", json={
        "routing_key": PAGERDUTY_KEY,
        "event_action": "trigger",
        "payload": {
            "summary": f"TAMPER DETECTED: {module_name}",
            "severity": "critical",
            "custom_details": {
                "expected": expected_hash,
                "actual": actual_hash,
            }
        }
    })

# Step 5: Activate
folija.activate()
```

---

### Scenario 8: CI/CD pipeline — verify build artifact before deploy

```bash
# In your deploy script or GitHub Actions step:

# 1. Create baseline from the build
folija baseline create dist/ --output dist/baseline.json

# 2. Sign or store baseline.json alongside your artifact

# 3. On the server, after deploy, verify:
folija report --baseline /srv/baselines/app.json --exit-code
# exits 0 if clean, 1 if tampered, 2 if files missing
echo $?
```

GitHub Actions example:

```yaml
- name: Verify deployment integrity
  run: |
    pip install folija
    folija baseline create ${{ github.workspace }}/app \
      --output baseline.json
    # Upload baseline as artifact
    
- name: Post-deploy integrity check
  run: |
    folija report --baseline baseline.json --exit-code --json
```

---

### Scenario 9: Container / Docker — immutable image check

```dockerfile
FROM python:3.12-slim
COPY app/ /srv/app/
RUN pip install folija && \
    folija baseline create /srv/app --output /srv/baselines/app.json
```

```python
# entrypoint.py
import folija
from folija.baseline import load_baseline
from folija.report import IntegrityReport

load_baseline("/srv/baselines/app.json")
folija.watch_directory("/srv/app/")
folija.activate()

# Abort if container filesystem was tampered since image build
report = IntegrityReport.run()
if not report.ok:
    print(report)
    raise SystemExit(f"Container integrity failure: {len(report.tampered)} tampered files")
```

---

### Scenario 10: Remote verification via REST API

Start the server on the protected host:

```bash
export FOLIJA_API_KEY="change-this-to-a-secret"
folija serve --host 0.0.0.0 --port 7654
```

Query from your monitoring infrastructure:

```bash
# Check status
curl -H "Authorization: Bearer $KEY" http://app-server:7654/status

# Verify specific file
curl -H "Authorization: Bearer $KEY" \
     -X POST http://app-server:7654/verify \
     -H "Content-Type: application/json" \
     -d '{"path": "/srv/app/payments/processor.py"}'

# Full report
curl -H "Authorization: Bearer $KEY" http://app-server:7654/report

# Batch verify
curl -H "Authorization: Bearer $KEY" \
     -X POST http://app-server:7654/verify/batch \
     -H "Content-Type: application/json" \
     -d '{"paths": ["/srv/app/payments.py", "/srv/app/auth.py", "/srv/app/crypto.py"]}'
```

---

### Scenario 11: Multiple environments, shared baseline

```python
# deploy/baseline_manager.py
import os
import folija
from folija.baseline import create_baseline, load_baseline

ENV = os.environ.get("APP_ENV", "production")
BASELINE_PATH = f"/srv/baselines/app_{ENV}.json"

def setup_integrity():
    """Call this once at application startup."""
    if not os.path.exists(BASELINE_PATH):
        # First deploy in this environment — create baseline
        result = create_baseline(
            paths=["/srv/app/"],
            output_path=BASELINE_PATH,
        )
        print(f"Baseline created: {len(result)} files")
    else:
        load_baseline(BASELINE_PATH)

    folija.watch_directory("/srv/app/")
    folija.activate(callbacks=[send_security_alert])
```

---

### Scenario 12: Monitor a third-party library you don't control

```python
import folija

# You want to know if stripe's library is modified on your system
import stripe  # import it first
folija.watch("stripe")
folija.watch("stripe.api_resources")
folija.watch("stripe._stripe_client")

# Immediately compute and store hashes as baseline
import stripe
from folija.baseline import create_baseline
import os

stripe_dir = os.path.dirname(stripe.__file__)
result = create_baseline([stripe_dir], output_path="/srv/baselines/stripe.json")
# Load it back (injects into folija's state)
from folija.baseline import load_baseline
load_baseline("/srv/baselines/stripe.json")

folija.activate()
# Now if anyone modifies stripe.py on your server — you know immediately.
```

---

### Scenario 13: .so / compiled extension integrity

```python
import folija

# Watch Cython/C extensions too
folija.watch("myapp._fast_crypto", "/srv/app/myapp/_fast_crypto.cpython-312.so")
folija.watch("myapp._parser",      "/srv/app/myapp/_parser.cpython-312.so")

# Or watch all .so files in a directory
folija.watch_directory("/srv/app/", pattern="*.so", recursive=True)

folija.activate()
```

---

### Scenario 14: Audit log — write every verification result

```python
import folija
import json
from datetime import datetime, timezone
from pathlib import Path

LOG_FILE = Path("/var/log/folija_audit.jsonl")

@folija.on_tamper
def audit_log(module, expected, actual):
    entry = {
        "ts": datetime.now(timezone.utc).isoformat(),
        "event": "TAMPER",
        "module": module,
        "expected": expected,
        "actual": actual,
    }
    with open(LOG_FILE, "a") as f:
        f.write(json.dumps(entry) + "\n")

folija.activate()
```

---

### Scenario 15: Periodic full integrity scan (Celery / APScheduler)

```python
# tasks.py (Celery)
from celery import shared_task
from folija.report import IntegrityReport

@shared_task
def integrity_scan():
    report = IntegrityReport.run()
    if not report.ok:
        # Send report to security team
        send_security_email(
            subject=f"Folija: {len(report.tampered)} tampered file(s)",
            body=str(report),
        )
    return report.to_dict()
```

```python
# APScheduler
from apscheduler.schedulers.background import BackgroundScheduler
from folija.report import IntegrityReport

scheduler = BackgroundScheduler()
scheduler.add_job(
    lambda: IntegrityReport.run().raise_if_tampered(),
    trigger="interval",
    minutes=5,
)
scheduler.start()
```

---

## How It Works — Deep Dive

### The Import Hook

Python resolves imports by asking each object in `sys.meta_path` in order.  
`Folija(MetaPathFinder)` inserts itself at position 0 — first in line.

```python
class Folija(importlib.abc.MetaPathFinder):
    def find_spec(self, fullname, path, target=None):
        if fullname in _watched:
            _check(fullname, _watched[fullname])
        return None  # NEVER intercept — only observe
```

`find_spec` returns `None` unconditionally. Python continues to the next finder and imports normally. Folija only observes.

### The Hash Check

```python
def _check(module_name, file_path):
    if file_path in _verified and file_path not in _tampered:
        return True   # cached clean — skip

    actual = hashlib.sha256(open(file_path, "rb").read()).hexdigest()
    expected = _baseline.get(file_path)

    if expected and actual != expected:
        _tampered.add(file_path)
        _fire_callbacks(module_name, expected, actual)
        return False

    _verified[file_path] = actual
    return True
```

Each file is hashed **at most once** per process lifetime (cached after first clean check). The check is ~1ms and never blocks.

### Thread Safety

All state updates go through a `threading.Lock`.  
Callbacks fire in a **daemon thread** — they can't block your import chain.

### The Baseline

A baseline is a plain JSON file:

```json
{
  "folija_version": "1.0.0",
  "created_at": "2026-04-23T10:00:00Z",
  "entry_count": 47,
  "entries": {
    "/srv/app/myapp/payments.py": "a3f8c2...",
    "/srv/app/myapp/auth.py":     "b91d44...",
    "/srv/app/myapp/crypto.py":   "c7e012..."
  }
}
```

Store it in version control, in S3, on a read-only mount — anywhere your application can read it at startup.

---

## CLI Reference

### `folija baseline create`

```
folija baseline create <paths...> --output <path.json> [--pattern "*.py"] [--no-recursive]

Arguments:
  paths         One or more directories or files to baseline
  --output, -o  Output JSON file path (required)
  --pattern     Glob pattern for directory scanning (default: *.py)
  --no-recursive  Only scan top-level, no subdirectories

Examples:
  folija baseline create /srv/app --output /srv/baselines/app.json
  folija baseline create /srv/app/payments.py /srv/app/auth.py --output baseline.json
  folija baseline create /srv/app --pattern "*.so" --output so_baseline.json
```

### `folija baseline load`

```
folija baseline load <path.json>

Arguments:
  path    Path to baseline JSON file

Examples:
  folija baseline load /srv/baselines/app.json
```

### `folija verify`

```
folija verify <paths...> [--hash <sha256>] [--baseline <path.json>] [--json]

Arguments:
  paths         One or more files to verify
  --hash        Expected SHA-256 (single file only)
  --baseline    Load this baseline before verifying
  --json        Output as JSON

Exit codes:
  0   All files clean
  1   One or more files tampered

Examples:
  folija verify /srv/app/payments.py
  folija verify /srv/app/payments.py --hash a3f8c2...
  folija verify /srv/app/payments.py --baseline /srv/baselines/app.json
  folija verify /srv/app/payments.py --json
```

### `folija report`

```
folija report [paths...] [--baseline <path.json>] [--json] [--exit-code]

Arguments:
  paths         Specific paths to check (default: all watched modules)
  --baseline    Load this baseline before reporting
  --json        Output as JSON
  --exit-code   Exit 0=clean, 1=tampered, 2=missing

Examples:
  folija report --baseline /srv/baselines/app.json
  folija report /srv/app/payments.py /srv/app/auth.py
  folija report --baseline /srv/baselines/app.json --json
  folija report --baseline /srv/baselines/app.json --exit-code
```

### `folija serve`

```
folija serve [--host HOST] [--port PORT] [--key API_KEY] [--reload]

Arguments:
  --host      Bind host (default: 127.0.0.1)
  --port      Bind port (default: 7654)
  --key       API key (sets FOLIJA_API_KEY env var)
  --reload    Enable auto-reload for development

Examples:
  folija serve                                    # localhost only, no auth
  folija serve --host 0.0.0.0 --port 7654 --key secret123
  FOLIJA_API_KEY=secret folija serve --host 0.0.0.0
```

### `folija status`

```
folija status

Output (JSON):
  active           bool   — membrane is in sys.meta_path
  watched          int    — modules registered for watching
  baseline_loaded  int    — entries in baseline
  verified         int    — files verified this process lifetime
  tampered         int    — files where tampering was detected
  tampered_modules list   — paths of tampered files
```

---

## REST API Reference

All endpoints require `Authorization: Bearer <API_KEY>` if `FOLIJA_API_KEY` is set.

| Method | Path | Description |
|--------|------|-------------|
| GET | `/` | Health check |
| GET | `/status` | Membrane status |
| GET | `/docs` | Interactive API docs (Swagger UI) |
| POST | `/verify` | Verify a file by absolute path |
| POST | `/verify/module` | Verify a watched module by name |
| POST | `/verify/batch` | Verify multiple files at once |
| GET | `/report` | Full integrity report |
| POST | `/watch` | Add a module to watch list |
| POST | `/watch/directory` | Watch a directory |
| POST | `/baseline/create` | Create a new baseline |
| POST | `/baseline/load` | Load a baseline file |

### POST `/verify`

```json
Request:
{
  "path": "/srv/app/payments.py",
  "expected_hash": "a3f8c2..."   // optional — overrides baseline
}

Response:
{
  "path": "/srv/app/payments.py",
  "ok": true,
  "status": "OK",
  "actual_hash": "a3f8c2...",
  "expected_hash": "a3f8c2..."
}
```

Status values: `OK` | `TAMPERED` | `NO_BASELINE` | `FILE_NOT_FOUND`

### POST `/verify/batch`

```json
Request:
{
  "paths": [
    "/srv/app/payments.py",
    "/srv/app/auth.py",
    "/srv/app/crypto.py"
  ]
}

Response:
{
  "ok": true,
  "count": 3,
  "results": [
    {"path": "...", "ok": true, "status": "OK", "actual_hash": "...", "expected_hash": "..."},
    {"path": "...", "ok": true, "status": "OK", "actual_hash": "...", "expected_hash": "..."},
    {"path": "...", "ok": false, "status": "TAMPERED", "actual_hash": "...", "expected_hash": "..."}
  ]
}
```

### GET `/report`

```json
Response:
{
  "ok": false,
  "generated_at": "2026-04-23T12:00:00Z",
  "summary": {
    "total": 47,
    "clean": 45,
    "tampered": 1,
    "missing": 1,
    "no_baseline": 0
  },
  "tampered": [
    {
      "path": "/srv/app/payments.py",
      "ok": false,
      "status": "TAMPERED",
      "actual_hash": "deadbeef...",
      "expected_hash": "a3f8c2..."
    }
  ],
  "missing": [
    {"path": "/srv/app/deleted_module.py", "ok": false, "status": "FILE_NOT_FOUND", ...}
  ],
  "no_baseline": [],
  "all_results": [...]
}
```

---

## Python API Reference

### `folija.watch(module_name, file_path=None)`

Register a module for integrity monitoring.

```python
folija.watch("myapp.payments")
folija.watch("myapp.auth", "/srv/app/myapp/auth.py")    # explicit path
folija.watch("myapp._parser", "/srv/app/myapp/_parser.cpython-312.so")  # .so files too
```

### `folija.watch_directory(path, pattern="*.py", recursive=True) → int`

Watch all matching files in a directory.

```python
count = folija.watch_directory("/srv/app/myapp/")
count = folija.watch_directory("/srv/app/", pattern="*.so")
count = folija.watch_directory("/srv/app/myapp/", recursive=False)  # top-level only
```

### `folija.activate(callbacks=None) → Folija`

Activate the membrane. Inserts `Folija` into `sys.meta_path`.

```python
membrane = folija.activate()
membrane = folija.activate(callbacks=[my_alert, my_log])
```

### `folija.deactivate()`

Remove the membrane from `sys.meta_path`.

```python
folija.deactivate()
```

### `folija.verify_file(path, expected_hash=None) → VerifyResult`

Verify a single file. Optionally override expected hash.

```python
result = folija.verify_file("/srv/app/payments.py")
result = folija.verify_file("/srv/app/payments.py", expected_hash="a3f8c2...")
if not result:
    print(f"TAMPERED: {result.status}")
```

### `folija.verify_module(module_name) → VerifyResult`

Verify a watched module by its Python dotted name.

```python
result = folija.verify_module("myapp.payments")
result = folija.verify_module("stripe")
```

### `folija.status() → dict`

Return membrane status.

```python
s = folija.status()
# {
#   "active": True,
#   "watched": 47,
#   "baseline_loaded": 47,
#   "verified": 42,
#   "tampered": 0,
#   "tampered_modules": []
# }
```

### `@folija.on_tamper`

Decorator to register a tamper callback.

```python
@folija.on_tamper
def my_alert(module_name: str, expected_hash: str, actual_hash: str):
    # Called asynchronously in a daemon thread
    send_pagerduty_alert(module_name, expected_hash, actual_hash)
```

### `VerifyResult`

```python
result.path           # str   — file path
result.ok             # bool  — True if clean or no baseline
result.status         # str   — "OK" | "TAMPERED" | "NO_BASELINE" | "FILE_NOT_FOUND" | "MODULE_NOT_FOUND"
result.actual_hash    # str | None  — SHA-256 of file as it is now
result.expected_hash  # str | None  — SHA-256 from baseline (if loaded)
bool(result)          # same as result.ok
repr(result)          # VerifyResult(ok=True, status='OK', path='/srv/app/...')
```

### `BaselineResult`

```python
result.path           # str   — baseline JSON file path
result.ok             # bool  — True if no errors
result.entries        # dict  — {file_path: sha256}
result.errors         # list  — errors encountered
result.created_at     # str   — ISO 8601 UTC timestamp
len(result)           # int   — number of entries
"/srv/app/x.py" in result   # bool — is file in baseline
result["/srv/app/x.py"]     # str  — SHA-256 of that file
```

### `IntegrityReport`

```python
report = IntegrityReport.run()           # check all watched files
report = IntegrityReport.run(paths=[...])  # check specific files only

report.ok              # bool
report.total           # int    — all files checked
report.clean           # int    — files with matching hash
report.tampered        # list[VerifyResult]
report.missing         # list[VerifyResult]
report.no_baseline     # list[VerifyResult]
report.exit_code       # int: 0=clean, 1=tampered, 2=missing
report.to_dict()       # dict
report.to_json()       # str (pretty JSON)
str(report)            # human-readable table
bool(report)           # same as report.ok
report.raise_if_tampered()  # raises IntegrityError if tampered
```

---

## Security Architecture

### What Folija Detects

- **In-place file modification** — any byte change to a watched `.py` or `.so` file
- **Monkey-patching via file system** — editing the file while the app is running
- **Supply chain substitution** — a dependency replaced with a modified version
- **Container filesystem tampering** — writable layer modifications after image build
- **Unauthorized hotfixes** — well-intentioned but unapproved changes
- **Insider threats** — developer modifies a file directly on the production server

### What Folija Does NOT Detect

- **In-memory monkey-patching** — `module.function = evil_function` at runtime (no file changes involved)
- **Import-time code injection** — malicious `.pth` files, `sitecustomize.py`, etc.
- **Baseline tampering** — if an attacker can modify your baseline file, they can hide their changes. Store your baseline securely.
- **Python bytecode injection** — modifying `.pyc` files only (Folija checks `.py` source by default)

### Defense in Depth

Folija is one layer. Pair it with:
- Read-only filesystem mounts for your app code
- AppArmor or SELinux profiles
- Immutable container images (rebuild don't patch)
- Code signing for your deployment artifacts
- Separate, signed storage for your baseline files

### Baseline Security

The baseline file is the root of trust. Protect it:

```bash
# Store baseline outside app directory, read-only
chmod 444 /srv/baselines/app.json

# Or store in version control (commit the baseline alongside your code)
git add .folija_baseline.json
git commit -m "chore: update folija baseline"

# Or generate at build time and embed in container image
# (then it's covered by container signing)
```

### Thread Safety

All writes to shared state go through `threading.Lock`.  
Callbacks fire in daemon threads — they cannot block your application or imports.

---

## Performance

| Operation | Typical latency |
|-----------|----------------|
| First hash check (file read + SHA-256) | ~0.5–2ms depending on file size |
| Subsequent checks (cached) | ~1µs (dict lookup) |
| Full report on 500 files | ~500ms (mostly I/O) |
| API server `/verify` endpoint | ~2–5ms (HTTP + hash) |

Caching ensures each file is hashed **at most once** per process lifetime, unless tampering is detected.

---

---

## Where Folija Applies — The Full Picture

Most security tools protect the perimeter. Firewalls, WAFs, authentication layers.  
They ask: *Who is trying to get in?*

Folija asks a different question: *Is the code that's running still the code we trust?*

This question matters in more places than most developers realize.

---

### Financial Systems — Payments, Banking, Trading

Every payment processor, every banking backend, every trading system is a target.  
Not just for external attackers. Supply chain. Insider threat. Compromised CI.

A single modified function in `payments/processor.py` — one that rounds down fractions and redirects them — can drain millions before any log shows anything unusual.

Folija detects the modification the next time that module is imported.  
Before the next transaction. Before the money moves.

**Applicable to:** payment gateways, core banking systems, accounting software, trading platforms, billing engines, crypto exchanges, POS systems, salary processing.

---

### Legal & Notary Systems — Where Code is Law

When code generates a legally binding document, the code itself has legal weight.  
A modified template. A silently altered signatory field. A hash function that's been weakened.

These are not theoretical. This is precisely the threat model for digital notarization,  
e-signature platforms, court document management, and legal tech in general.

If your system generates contracts, wills, deeds, or certificates —  
the integrity of the code generating them is as important as the documents themselves.

**Applicable to:** notary platforms, e-signature systems, court management software, deed registries, contract lifecycle management, digital diploma issuance, legal document generation, will management (especially: digital oporuka/testament vaults).

---

### Healthcare — Patients Cannot Wait for a Postmortem

A dosage calculation function. A diagnostic algorithm. A drug interaction checker.  
A modified triage classifier in an emergency department.

Healthcare software rarely undergoes runtime integrity checks because "it's behind the firewall."  
But the firewall doesn't protect against a compromised dependency update that lands in production at 3am.

**Applicable to:** EHR/EMR systems, pharmacy management, dosage calculators, laboratory information systems, telemedicine platforms, medical device firmware (where Python is used), clinical decision support.

---

### Government & Public Infrastructure

Voting systems. Tax processing. Benefit distribution. Social registry.  
Public procurement. National identity management.

These systems are high-value targets and their integrity is a matter of democratic legitimacy, not just technical correctness. A modified rounding function in tax software affects every citizen.

Folija gives operators a cryptographic guarantee that the running code matches what was audited and deployed.

**Applicable to:** e-voting, tax administration, social benefit systems, national registries (land, business, population), customs, public procurement platforms, digital identity infrastructure.

---

### Education & Certification

Universities issuing digital diplomas. Certification bodies.  
Testing platforms where a modified grading function changes who passes.

A compromised exam scoring algorithm is a corruption case waiting to happen.  
A diploma issuance system where the signing code is modified means every diploma issued after the compromise is suspect.

**Applicable to:** digital diploma platforms, certification management, online testing and proctoring, student information systems, continuing education platforms.

---

### Insurance — Actuarial Code as Evidence

Actuarial calculations are used to set premiums, deny claims, settle disputes.  
If that code is modified — intentionally or through a supply chain compromise —  
the company may not even know which policies were priced incorrectly.

In regulatory investigations, being able to prove that the code that ran on a given date  
was the audited, baseline-approved version is the difference between a fine and a licence revocation.

**Applicable to:** underwriting engines, claims processing systems, actuarial calculation platforms, fraud detection, reinsurance systems.

---

### Critical Infrastructure — Energy, Water, Transport

Industrial control systems increasingly run Python at their edge nodes and HMIs.  
SCADA systems. Smart grid management. Traffic control. Water treatment monitoring.

The attack surface is the code itself. Folija's `watch_directory` on the edge node  
catches a modified control loop before the next execution cycle.

**Applicable to:** SCADA/HMI interfaces, energy management systems, smart metering, water treatment control systems, railway management software, air traffic control auxiliary systems.

---

### Multi-Tenant SaaS — Protecting Every Customer

In a multi-tenant SaaS platform, a compromise of the core billing, authentication,  
or permission code affects every customer simultaneously.

Folija's callback model means you can alert your security team the moment  
a watched module is tampered — before the next request that would use the compromised code.

**Applicable to:** any SaaS platform with high-value tenants, HR platforms, CRM systems, accounting SaaS, ERP systems, document management SaaS.

---

### Open Source Libraries — Protecting Your Users

If you maintain an open source library, you can ship a baseline alongside your release  
and document how users can verify their installed copy matches what you signed.

```bash
pip install mylib
folija baseline load $(python -c "import mylib; print(mylib.__file__.replace('__init__.py', ''))")
```

This is supply chain defense at the library level. PyPI packages get modified.  
Mirrors get compromised. This gives your users a way to verify.

**Applicable to:** any widely-used Python library that handles sensitive operations — auth, crypto, payments, signing, validation.

---

### Embedded & Edge Computing

Raspberry Pi deployments. Industrial edge nodes. IoT gateways.  
Python runs on more hardware than most people realize.

Folija's zero-dependency core means it runs on a Raspberry Pi 4 just as well as  
a 64-core cloud server. Hash a baseline at provisioning time. Detect modifications at runtime.  
Alert back to your monitoring infrastructure via the REST API.

**Applicable to:** IoT gateways, smart factory edge nodes, agricultural monitoring systems, environmental sensors with local processing, point-of-sale terminal software.

---

### Archive & Document Preservation

The OpenDOK-2026 standard (from the same foundation as Folija) defines immortal documents.  
Folija is the runtime companion — it ensures the *software* reading and processing those documents  
is as trustworthy as the documents themselves.

A digital archive that verifies document integrity but runs on unverified code is only  
as trustworthy as its weakest link.

**Applicable to:** national archives, library digital preservation systems, notarial archives, estate management platforms, genealogy platforms, historical record systems.

---

### The Common Thread

Folija is not about paranoia. It is about **auditability**.

In every domain above, there is a question that an auditor, regulator, or judge  
may one day ask: *"How do you know the code that ran was the code you certified?"*

With Folija and a signed baseline, the answer is:  
*"Because we have a cryptographic record of every module hash, and none of them deviated."*

That answer is worth more than any firewall.

---

## Contributing

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

```bash
git clone https://github.com/opendok/folija
cd folija
pip install -e ".[dev]"
pytest
```

Tests require no internet access and no external services.

---

## Changelog

### 1.0.0 — 2026-04-23

- Initial public release
- `folija.core`: `watch`, `watch_directory`, `activate`, `deactivate`, `verify_file`, `verify_module`, `status`, `on_tamper`
- `folija.baseline`: `create_baseline`, `load_baseline`, `update_baseline`
- `folija.report`: `IntegrityReport`, `IntegrityError`
- `folija.middleware.django_middleware`: Django WSGI middleware
- `folija.middleware.flask_middleware`: Flask extension
- `folija.middleware.fastapi_middleware`: FastAPI/ASGI middleware
- `folija.api.server`: Remote integrity REST API (FastAPI)
- `folija.cli`: CLI (`folija baseline`, `folija verify`, `folija report`, `folija serve`, `folija status`)

---

## License

MIT — see [LICENSE](LICENSE).

Built by the [OpenDOK Foundation](https://opendok.org) and contributors.

---

*Folija (Croatian/Bosnian) — foil, membrane, thin protective layer.*  
*Named for what it does: wraps your runtime and keeps it whole.*

---

*"Measure twice, deploy once. Then verify it's still what you measured."*
