audit: wrap lockfile read in try/except OSError

Opus reviewer 3 caught a crash path: when a lockfile is unreadable
(chmod 000, permission denied, is-a-directory, broken pipe, etc.)
the audit script bubbles up a raw Python traceback and exits 1 with
no Finding emitted. In CI, a transient permission or encoding error
is indistinguishable from a malicious lockfile -- the maintainer reads
the log and sees noise, not a structured diagnosis.

Wraps both path.read_text() calls (npm and cargo branches) in a
try/except OSError that appends a new Finding kind
"unreadable-lockfile" with the original errno detail, mirroring how
missing-lockfile is handled today. Backward compatible (additive);
all 24 existing audit fixtures still pass.

Verified by fixture: chmod 000 on a lockfile -> exit 1 with
[unreadable-lockfile] finding, no traceback.
This commit is contained in:
Daniel Han 2026-05-19 01:42:05 +00:00
commit 1f397f45df

View file

@ -412,7 +412,20 @@ def audit_npm_lockfile(path: Path) -> list[Finding]:
)
return findings
raw = path.read_text(encoding = "utf-8")
try:
raw = path.read_text(encoding = "utf-8")
except OSError as exc:
# Permission denied, is-a-directory, broken-pipe etc. -- surface
# as a finding instead of crashing CI with a raw traceback.
findings.append(
Finding(
path = str(path),
package = "<root>",
kind = "unreadable-lockfile",
detail = f"could not read file: {exc}",
)
)
return findings
try:
lock = json.loads(raw)
except json.JSONDecodeError as exc:
@ -580,7 +593,18 @@ def audit_cargo_lockfile(path: Path) -> list[Finding]:
)
return findings
raw = path.read_text(encoding = "utf-8")
try:
raw = path.read_text(encoding = "utf-8")
except OSError as exc:
findings.append(
Finding(
path = str(path),
package = "<root>",
kind = "unreadable-lockfile",
detail = f"could not read file: {exc}",
)
)
return findings
try:
import tomllib # type: ignore[import-not-found]
except ImportError: