Add host correlation and assurance coverage
This commit is contained in:
parent
8194d13734
commit
b40ac4252c
36 changed files with 944 additions and 23 deletions
91
enodia_sentinel/assurance.py
Normal file
91
enodia_sentinel/assurance.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Append-only hash-chain records for local forensic artifacts."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from . import schemas
|
||||
|
||||
CHAIN_NAME = "hash-chain.jsonl"
|
||||
|
||||
|
||||
def chain_path(cfg) -> Path:
|
||||
return cfg.log_dir / CHAIN_NAME
|
||||
|
||||
|
||||
def _sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def file_digest(path: Path) -> str:
|
||||
return _sha256_bytes(path.read_bytes())
|
||||
|
||||
|
||||
def last_chain_hash(cfg) -> str:
|
||||
try:
|
||||
lines = chain_path(cfg).read_text().splitlines()
|
||||
except OSError:
|
||||
return ""
|
||||
for line in reversed(lines):
|
||||
try:
|
||||
record = json.loads(line)
|
||||
except ValueError:
|
||||
continue
|
||||
value = record.get("chain_hash")
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return ""
|
||||
|
||||
|
||||
def append_artifact(cfg, kind: str, path: Path) -> dict:
|
||||
"""Append a tamper-evident hash-chain record for ``path``."""
|
||||
cfg.log_dir.mkdir(parents=True, exist_ok=True)
|
||||
prev = last_chain_hash(cfg)
|
||||
try:
|
||||
digest = file_digest(path)
|
||||
size = path.stat().st_size
|
||||
except OSError:
|
||||
digest = ""
|
||||
size = 0
|
||||
record = {
|
||||
"schema": schemas.HASH_CHAIN_V1,
|
||||
"time": time.time(),
|
||||
"kind": kind,
|
||||
"path": str(path),
|
||||
"size": size,
|
||||
"sha256": digest,
|
||||
"prev_hash": prev,
|
||||
}
|
||||
payload = json.dumps(record, sort_keys=True, separators=(",", ":")).encode()
|
||||
record["chain_hash"] = _sha256_bytes(payload + prev.encode())
|
||||
with chain_path(cfg).open("a") as fh:
|
||||
fh.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
try:
|
||||
chain_path(cfg).chmod(0o640)
|
||||
except OSError:
|
||||
pass
|
||||
return record
|
||||
|
||||
|
||||
def summary(cfg) -> dict:
|
||||
path = chain_path(cfg)
|
||||
try:
|
||||
lines = path.read_text().splitlines()
|
||||
except OSError:
|
||||
return {"path": str(path), "exists": False, "count": 0, "last_hash": ""}
|
||||
last = ""
|
||||
for line in reversed(lines):
|
||||
try:
|
||||
last = json.loads(line).get("chain_hash", "")
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
return {
|
||||
"path": str(path),
|
||||
"exists": True,
|
||||
"count": len(lines),
|
||||
"last_hash": last,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue