Skip to main content

Runtime Integrity Membrane for Python — know when your code changes

Project description

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 Python License: MIT Zero dependencies Downloads


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

# 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

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

# 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

# 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

# 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:

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

# 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+):

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)

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

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

# 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:

- 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

FROM python:3.12-slim
COPY app/ /srv/app/
RUN pip install folija && \
    folija baseline create /srv/app --output /srv/baselines/app.json
# 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:

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

Query from your monitoring infrastructure:

# 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

# 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

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

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

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)

# 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()
# 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.

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

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:

{
  "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

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

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

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.

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.

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.

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

folija.deactivate()

Remove the membrane from sys.meta_path.

folija.deactivate()

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

Verify a single file. Optionally override expected hash.

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.

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

folija.status() → dict

Return membrane status.

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.

@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

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

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

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-patchingmodule.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:

# 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.

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.

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.

Built by the OpenDOK Foundation 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."

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

folija-1.0.0.tar.gz (53.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

folija-1.0.0-py3-none-any.whl (31.6 kB view details)

Uploaded Python 3

File details

Details for the file folija-1.0.0.tar.gz.

File metadata

  • Download URL: folija-1.0.0.tar.gz
  • Upload date:
  • Size: 53.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.0

File hashes

Hashes for folija-1.0.0.tar.gz
Algorithm Hash digest
SHA256 b7765f7c0ed8dbbc07bb16109bba24b5d558a4792820780c7a2da97427d83d34
MD5 6d8400245ee981b871d38addd79bb27d
BLAKE2b-256 cc84a33a1ac131505998a866eb92fdb5820eba80111c216b89f54ff05a604ff1

See more details on using hashes here.

File details

Details for the file folija-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: folija-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 31.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.0

File hashes

Hashes for folija-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f9e96ae74a9d5d8a1216bc535e502c6f6b57c7e011856ad15bca3d778bae85cd
MD5 6e577cab57c18a5258872b68e2bb92ae
BLAKE2b-256 509dd8fdc346da6a13e3ea677ce2c1704a200b3691e343db6d867140778fa38d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page