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."
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:
- Baselining your deployed files — computing SHA-256 of every
.py(or.so) file you care about. - Sitting silently in
sys.meta_path(Python's import hook chain). - Checking the hash every time Python imports a watched module.
- 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
.pyor.sofile - 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_functionat runtime (no file changes involved) - Import-time code injection — malicious
.pthfiles,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
.pycfiles only (Folija checks.pysource 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_tamperfolija.baseline:create_baseline,load_baseline,update_baselinefolija.report:IntegrityReport,IntegrityErrorfolija.middleware.django_middleware: Django WSGI middlewarefolija.middleware.flask_middleware: Flask extensionfolija.middleware.fastapi_middleware: FastAPI/ASGI middlewarefolija.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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7765f7c0ed8dbbc07bb16109bba24b5d558a4792820780c7a2da97427d83d34
|
|
| MD5 |
6d8400245ee981b871d38addd79bb27d
|
|
| BLAKE2b-256 |
cc84a33a1ac131505998a866eb92fdb5820eba80111c216b89f54ff05a604ff1
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9e96ae74a9d5d8a1216bc535e502c6f6b57c7e011856ad15bca3d778bae85cd
|
|
| MD5 |
6e577cab57c18a5258872b68e2bb92ae
|
|
| BLAKE2b-256 |
509dd8fdc346da6a13e3ea677ce2c1704a200b3691e343db6d867140778fa38d
|