Repository navigation
Releases: chriswayneh/linchpin
Releases · chriswayneh/linchpin
Release list
v0.2.1
Fixed
- ID002: a duplicate Entra/Okta record can no longer hide a disabled account. Every duplicate is
checked and ID002 fires if any is disabled, whatever the file order (0.2.0 used only the
first record). The demo now shows ID002 for the duplicate Okta user. - SIG001 ignores events after
--now, matching SIG002. - ID003/ID004 findings are labeled with the identifier the source correlates on (Entra
mail/userPrincipalName, Oktaemail/login), not a strayemailfield. - Table output caps column padding at 60 characters; one long value no longer pads every row
(longer values are shown in full, never truncated). - SIG002 indexes events by email once instead of scanning all events per leaver.
- Loader re-checks the opened file descriptor (non-blocking open + fstat), so a path swapped
for a FIFO after theis_file()check is refused instead of blocking.
Changed
- JSON/CSV output: ID005 evidence key
used_for_matchingis renamedfirst_index(the index
of the first duplicate in file order; informational only). - ID002 evidence values are lists of every disabled value across duplicate records,
deduplicated and sorted, e.g."okta.status": ["DEPROVISIONED", "SUSPENDED"]and
"entra.accountEnabled": [false](were single values).
Docs
- README: duplicate policy updated; missing export files make results incomplete.
v0.2.0
Added
- ID005 (medium): duplicate identity. When several records in one export (Entra, Okta, Crowd
or Bitbucket users) normalize to the same email, one finding lists every duplicate record
(index and identifier/status fields) andused_for_matching. --format markdown: a report with summary counts by severity and by rule, then a findings
table. Cells strip control characters and line separators, entity-escape&,<,>,
backslash-escape Markdown syntax including|, and write:,.,/,@as character
references so GFM and linkify renderers produce no autolinks, links or images.- Demo data includes a duplicate Okta record so ID005 appears in
identity --demo.
Changed
- U+2028/U+2029 (Unicode line/paragraph separators, Zl/Zp) are treated as control characters:
rejected in identity fields and stripped from all output formats. - Duplicate policy: the first record in file order is used for Entra/Okta matching (was a
silent last-record-wins). This can change ID002 results for exports with duplicates. - ROADMAP: v0.2 is the duplicate-identity and Markdown work; later phases renumbered (real
exports v0.3, correlation v0.4, rules config v0.5, CI gating v0.6, reporting v0.7).
v0.1.2
Fixed
- MFA, privilege, enabled and active flags (
mfaRegistered,isPrivileged,accountEnabled,
mfaEnrolled,isAdmin,active) must be JSON booleans or null; a string such as
"false"was treated as true. Other types now raise a clear error (exit 2). - Every identifier field the rules read (
email,mail,userPrincipalName,login,
name,username) is type- and control-character-checked in every export, so a record such
as{"mail": "a@x.com", "email": 42}exits 2 instead of crashing. Display names skip
non-string values. - Okta
statusmust be a string or null (with the control-character check); other types exit 2.
A null or missing Oktastatusor EntraaccountEnabledis treated as unknown: it no longer
fires a false ID002 or drops the account from ID003. A null Crowdactivealso stays in
ID003 (ID003 covers Entra, Okta and Crowd only). - Any
UnicodeErrorwhile decoding an export (not onlyUnicodeDecodeError) exits 2.
v0.1.1
Security (Red Team review)
- Control-character checks now cover Unicode categories Cc, Cf and Cs: C1 controls
(U+0080–U+009F, including CSI U+009B), bidi overrides/isolates (U+202A–U+202E,
U+2066–U+2069), zero-width characters and unpaired surrogates. Identity fields containing
them are rejected (exit 2) with the code point named, e.g.contains U+200D; table and CSV
output strip them. - Invalid UTF-8, deeply nested JSON (RecursionError), integer literals over Python's
int digit limit (ValueError) and timestamps with extreme UTC offsets (OverflowError) now
raise a clearLoadError(exit 2) instead of a traceback. - Exports must be regular files (directories and devices such as a symlink to
/dev/zero
are refused). At most 200 MB + 1 byte is read, so oversized files are refused without
trustingst_size; read errors (OSError) exit 2. --nowvalues that overflow, are too early for the stale/leaver window, or are too early
to generate--demodata (which is dated up to ~200 days before--now), are rejected
with exit 2.- Demo timestamps zero-pad years below 1000 so they round-trip through the parser.
- CSV formula-injection guard ignores leading whitespace (e.g.
" =cmd").
Added
SECURITY.mddescribing private vulnerability reporting.
v0.1.0
Added
linchpin identitycommand: ID001 orphaned app account, ID002 disabled in IdP but active
in app, ID003 stale login, ID004 privileged without MFA.linchpin signalscommand: SIG001 off-hours mass repo access, SIG002 recent-leaver activity.--demoseeded, deterministic fake exports across Entra ID, Okta, Atlassian Crowd, Bitbucket.--data DIRloader forentra.json,okta.json,crowd.json,bitbucket.json.- Explainable findings (
rule_id,severity,user,source,reason,evidence). - Output formats: table, JSON, CSV (CSV cells guarded against formula injection).
- Tooling: pytest with branch coverage and hypothesis (fail-under 85), ruff, mypy (strict),
bandit, pip-audit.
Fixed (QE review of PR #1)
- Invalid timestamps in
--dataexports raise a clear error naming file and field (exit 2). - Export entries must be JSON objects; Bitbucket
users/eventsmust be arrays. - SIG002 evidence ignores missing/
Nonerepo values (previously crashed sorting). - mypy error in repo collection fixed via
isinstancenarrowing. --stale-daysand--mass-thresholdmust be >= 1;--leaver-window-days>= 0.- Accounts with empty emails are no longer correlated with each other; their ID003/ID004
findings show the lowercased login, name, or username instead, else(no email).
Signals skip anonymous data: SIG001 never merges events without an email, and SIG002
never matches a leaver without an email, so no SIG finding has an empty user. - Non-string email/identity fields in exports raise a clear error (exit 2).
- CSV output strips control characters (NUL crashed the csv writer on Python 3.10).
- IdP correlation normalizes before choosing: a blank/whitespace
mail(Entra) oremail
(Okta) falls back touserPrincipalName/logininstead of dropping the account. - SIG002 fires only for leave dates within
[now - window, now]and events strictly after
the leave date and not afternow; future leave dates or events never fire. - SIG001
first/lastand SIG002last_eventare chosen by parsed UTC time, not string
order, so mixed UTC offsets are reported correctly. - Control characters in identity fields are rejected on load (exit 2); table output also
strips them as defense against terminal escape injection.