Repository navigation
fix(sieve): resolve case-exact file probes and deduplicate contents (fixes #645) - #658
Conversation
Marc-cn
left a comment
There was a problem hiding this comment.
Thanks, the approach is the one #645 asks for and the core of it reads right: case-exact traversal against os.listdir, the listing cache keyed by directory mtime, dedup by (st_dev, st_ino). 160 tests pass here.
Two things before merge:
- Please drop the unrelated reformatting. In
core/utils.pyabout 60 of the 169 changed lines are untouched code rewritten by a formatter (quotes, joined argument lists, split f-strings), andsieve/builtin_handlers.pyhas the same. The project isn't formatted withruff format, and these hunks hide the real change and conflict with every other PR touching those files. - The TOML change isn't only about case: to stay at 5 entries,
files_to_includedropsdocs/design.md(OSPS-SA-01.01) andswagger.json(OSPS-SA-02.01), so the LLM step stops reading those. @mlieberman85, that's a content call for you; if those files should stay, the uppercase variants could go intodiscover/filesonly.
Note for Mike: this edits discover_files in config/discovery.py, which #630's notes mark for removal after #622.
|
Thanks @Marc-cn! Addressed both points in 0c37b6c:
All 160 tests in |
0c37b6c to
c91e235
Compare
|
Both of my points are addressed: the formatting churn is gone and files_to_include matches main. The case-exact walk only filters candidates, the reads still go through the #635 containment, so the two compose. @mlieberman85: the uppercase variants added to the Baseline discover/files lists, and whether a 400-line change for case-insensitive filesystems is wanted given darnit mostly runs in Linux containers. The dedup of contents is the part I'd keep regardless. |
c91e235 to
79ae049
Compare
|
Rebased cleanly onto latest |
Marc-cn
left a comment
There was a problem hiding this comment.
Right approach for #645: each component checked against the real directory listing, dedupe by inode before reading into evidence; tests cover it. Four things:
- The three test files carry a lot of unrelated reformatting (string wrapping, quotes). Please drop it so the diff is only the fix.
builtin_handlers.pyimports the private_dir_entriesfromcore.utils; make it public now that it's cross-module.- The listing cache is keyed on
st_mtime_ns. On coarse-mtime filesystems (HFS+, FAT, some network mounts) a file written in the same tick as the listing stays invisible, and the re-check after a remediation apply is exactly that pattern.clear_dir_entries_cache()exists but nothing on the apply/re-check path calls it. Either clear it after an apply or don't cache across that boundary. - The depth-walk branches now test
pattern in dir_entries(dirpath)withoutos.path.exists, so a broken symlink entry counts as found where it didn't before.
Needs a rebase too. @mlieberman85 worth alook since it changes file resolution for every control.
79ae049 to
0dc46f8
Compare
|
Thanks @Marc-cn! Addressed all four points in
All 2,063 unit tests pass cleanly, and Ruff linting is green. |
Marc-cn
left a comment
There was a problem hiding this comment.
All four addressed: reformat reverted, dir_entries public, cache cleared on apply/write/re-check, os.path.exists back in the depth walks with the broken-symlink test. Please rebase on main to pick up #694; the Test failure shown is the old runner issue. @mlieberman85 one last check since it changes file resolution for every control, then merge.
0dc46f8 to
a758c39
Compare
a758c39 to
e34fd28
Compare
Marc-cn
left a comment
There was a problem hiding this comment.
The fix is right: checking each component against os.listdir gives Linux semantics on macOS and Windows, the listing cache is keyed by directory mtime and cleared after remediation writes, dedup by (st_dev, st_ino) covers case aliases and symlinks, containment is still enforced by _skip_outside, and the docs/ARCHITECTURE.md / docs/API.md TOML variants keep what macOS used to match by accident. Linux: ruff, validate_sync and the sieve/baseline/core suites pass (1834).
Since the Linux tests can't fail for a case mismatch (the OS already rejects it), I also ran the new tests on a Windows checkout, where it matters. The case-exact rejections pass there — good. Two of the new tests fail on Windows for an unrelated reason: they assert relative_path == "docs/ARCHITECTURE.md" and Windows gives docs\ARCHITECTURE.md:
TestFileExistsHandler::test_nested_case_exact_rejection(test_builtin_handlers.py:125)TestFileExistsHandler::test_glob_case_exact_match(test_builtin_handlers.py:136)
Please compare with Path(...) or os.path.normpath so the tests hold on the platforms the fix is for. And please use the PR template (.github/pull_request_template.md), including the AI-assistance section. Happy to approve after those two.
e34fd28 to
a361b93
Compare
|
Thanks @Marc-cn! Addressed both points in
All 5,139 tests across the repository pass cleanly locally, |
mlieberman85
left a comment
There was a problem hiding this comment.
Reviewed a361b93. The case-exact walk and the dedup work for ordinary paths, the shipped controls are unaffected, and the template and the Windows assertion changes from Marco's review are in. I can't run Windows and PR CI is Linux only, so nothing here confirms those two tests pass there. Requesting changes for three regressions that case-exactness introduced, plus two smaller items. Item 1 can produce a false PASS.
All of the results below come from darnit audit -f <framework>.toml on a throwaway repo, comparing main (075556e) with this PR.
1. max_depth > 0 with a path entry no longer finds the file (every filesystem).
builtin_handlers.py:304 (file_exists), :809 (_regex_exclude_evidence) and :899 (_resolve_regex_files) do pattern in dir_entries(dirpath). dir_entries() returns the names in one directory, so an entry like dist/app.exe never matches, not even at the root. Main joined the path and checked it on disk. framework-design.md:414 and :525 don't restrict max_depth to basenames.
On a repo where dist/app.exe exists:
| step | main | this PR |
|---|---|---|
pattern, exclude_files = ["dist/app.exe"], max_depth = 1, expr = 'output.files_found == 0', existence = true |
FAIL | PASS, dispositive |
same without max_depth |
FAIL | FAIL |
file_exists, files = ["dist/app.exe"], max_depth = 1, existence = true |
PASS | FAIL |
The first row has the shape of OSPS-QA-05.01: a forbidden file is present and the control passes. No shipped TOML hits this today, because QA-05.01's entries are globs and BR-05.01 and QA-02.01 use basenames, but an operator custom control or another framework would.
Suggested fix: resolve os.path.join(os.path.relpath(dirpath, root), pattern) with resolve_case_exact_path in those three branches, then drop the unused dir_entries import. With that applied, all three rows match main and tests/darnit/sieve passes. Please add tests with a slash-containing entry plus max_depth, including the exclude case.
2. Paths outside the repository are no longer recorded (spec 3.0.4).
resolve_case_exact_path returns None for a path outside the repository, so _skip_outside never sees it:
| step | main skipped_outside_repository |
this PR |
|---|---|---|
file_exists, files = ["../outside.md"] |
outside.md | none |
pattern, files = ["../outside.md"] |
outside.md | none |
llm_eval, files_to_include = ["../outside.md"] |
outside.md | none |
llm_eval, files_to_include = ["dangling.md"], a symlink pointing outside the repo that dangles |
dangling.md | none |
Nothing is read either way, and statuses don't change, but the spec requires the record so an auditor can see the attempt. It needs to be recorded before the case-exact check rejects the path.
3. A file name starting with two dots is rejected.
utils.py:760 tests rel.startswith(".."), so ..b.md counts as outside the repository. file_exists, files = ["..b.md"], existence = true: main PASS, this PR FAIL. Testing for rel == ".." or rel.startswith(".." + os.sep) should fix it, though I haven't run that.
4. SA-01.01 and SA-02.01: the LLM step isn't given the document the pattern step found.
The new TOML variants keep what macOS used to match by accident for the pattern step, but not for the LLM step. The uppercase names went into discover/files but not files_to_include, which still names only lowercase docs/architecture.md / docs/api.md for those documents. With case-exact resolution those no longer open docs/ARCHITECTURE.md on macOS. The resolved_files fallback only runs when nothing was read, so a repo with a README.md doesn't reach it. On wtf-frontend (which has docs/ARCHITECTURE.md and docs/API.md):
| control | main file_contents |
this PR file_contents |
|---|---|---|
| OSPS-SA-01.01 | README.md, docs/architecture.md | README.md |
| OSPS-SA-02.01 | README.md, docs/api.md | README.md |
The statuses don't change in that run (PENDING both ways), and file_contents matches what main reads on a case-sensitive volume, so it fails safe. But the pattern step now cites a document the judgment isn't given, on every filesystem. Suggestion: use $RESOLVED_FILES in files_to_include in place of those doc entries, listed after README.md, because the 5-file cap can otherwise push README out.
5. Spec. The file_exists, regex/pattern, llm_eval and llm_extract steps and discover_files are now case-exact on every filesystem, and repeated paths to one file are deduplicated. Plugin authors can observe that, so a sentence in those sections of framework-design.md would cover it. It shouldn't say every probe: on macOS, core.utils.file_exists still matches README.md for readme.md. The template's framework-spec box is ticked, but that file isn't in the diff.
Minor, non-blocking:
- No test pins the
clear_dir_entries_cache()calls inexecutor._write,executeandorchestrator._recheck: with all of them removed, the remediation, core and sieve tests still pass. - With
get_file_identitymade to return a unique value, onlytest_resolve_regex_files_deduplicates_same_fileand that function's own unit test fail in the sieve and core tests, sollm_evalandllm_extractdedup aren't pinned. - From reading the code, not run: the direct
file_existsbranch now reportsrelative_paththroughrelpath, so on Windows it would hold backslashes, andcontrol_loader.py:183feeds that value toproject_update. ThePath(...)assertions wouldn't catch it.
On a361b93, which is based on 075556e: full suite 5146 passed, ruff and validate_sync pass. A real-repo audit of wtf-frontend shows no status changes against 075556e, and on a case-insensitive volume the statuses and file_contents match main on a case-sensitive volume.
Drafted with AI assistance (Claude), reviewed and filed by me.
a361b93 to
1490162
Compare
|
Addressed all review feedback in
Full test suite (5,146 tests passed locally) and |
Marc-cn
left a comment
There was a problem hiding this comment.
Approved on 1490162. Both points addressed, and better than asked: relative_path is now normalised to "/" in the handler itself, so evidence is identical on every platform instead of the tests tolerating a difference. The max_depth lookups going through resolve_case_exact_path on the constructed relative path is the right generalisation, and the switch of SA-01.01/SA-02.01 to $RESOLVED_FILES matches the other eight controls that already use it. Linux: ruff, validate_sync, sieve/baseline/core suites and the corpus gate pass; the earlier Windows run confirmed the case-exact rejections where they matter.
mlieberman85
left a comment
There was a problem hiding this comment.
Reviewed 1490162 merged into main at 155dcc1. The four behavior problems from my last review are fixed.
| step | main | this PR |
|---|---|---|
pattern, exclude_files = ["dist/app.exe"], max_depth = 1, existence = true |
FAIL | FAIL |
file_exists, files = ["dist/app.exe"], max_depth = 1, existence = true |
PASS | PASS |
file_exists, files = ["..b.md"], existence = true |
PASS | PASS |
file_exists, files = ["LOWERCASE.md"] on a case-insensitive volume, where the file is lowercase.md |
PASS | FAIL |
| same on a case-sensitive volume | FAIL | FAIL |
skipped_outside_repository matches main for file_exists, pattern and llm_eval with ../outside.md, and for llm_eval with a dangling symlink. All of the above ran on both a case-insensitive and a case-sensitive volume. On a repo with docs/architecture.md and docs/api.md, on a case-insensitive volume, SA-01.01 and SA-02.01 now give the judgment step the same files as main.
Real repositories, every control against main:
jonschlinkert/is-numberandjonschlinkert/is-odd: no status changes on a case-insensitive volume.sindresorhus/slashon a case-insensitive volume: four controls change, because its files arelicenseand.github/security.md. OSPS-LE-01.01 goes WARN to FAIL, OSPS-LE-03.01 goes PASS to FAIL, and OSPS-VM-02.01 and STAGE1-REF-SECURITY-01 go PENDING to FAIL.sindresorhus/slashon a case-sensitive volume: main and this PR give the same status on every control, including those four FAILs.
So this makes macOS match what Linux already reports, as intended. Those FAILs are still wrong for that repository, since GitHub detects MIT in license. That predates this PR, so I'll file it separately rather than hold this up.
The full suite passes on the merged tree (5175 passed, 11 skipped), and so do ruff and validate_sync.
One thing before merge: please add an Assisted-by trailer to the commit, as AI_POLICY.md asks. The description names Claude and Gemini, but the commit has only the sign-off. It still merges cleanly into main, so it can go in once that's pushed; I'll resolve the conflicts with my open PRs on my side.
I didn't recheck the two non-blocking test gaps from my last review.
Drafted with AI assistance (Claude), reviewed and filed by me. Measurements quoted were executed and verified.
…ixes darnitdevorg#645) On case-insensitive filesystems (such as macOS APFS and Windows NTFS), os.path.exists() and open() match lowercase file probes against uppercase files in the repository. As reported in darnitdevorg#645, this causes sieve handlers to: 1. Match candidate probes that don't match the repository's actual case. 2. Read duplicate file contents in llm_eval_handler and llm_extract_handler when files_to_include or files specify both case variants. 3. Cite paths in audit evidence that do not exist under exact case in the repo. Changes: - Added resolve_case_exact_path(path, root=None) in darnit.core.utils. - Cached directory entries via public dir_entries(dir_path) keyed by st_mtime_ns. - Cleared directory entries cache on remediation apply and in orchestrator _recheck. - Added get_file_identity(full_path) returning (st_dev, st_ino) for physical file deduplication. - file_exists_handler: verifies exact case for globs, max_depth walks (including slash-containing entries like dist/app.exe), and direct path lookups while filtering broken symlinks, normalizing relative_path to POSIX forward slashes, and recording escaped paths in skipped_outside_repository per spec 3.0.4. - _resolve_regex_files & _regex_exclude_evidence: ensures exact case matches, supports slash-containing entries in max_depth walks, filters broken symlinks, records outside paths, and deduplicates by physical file identity. - llm_eval_handler & llm_extract_handler: enforces case-exact paths, records outside paths/dangling symlinks in skipped_outside_repository, and deduplicates opened files before adding to file_contents. - utils.py: recognized filenames prefixed with two dots (e.g. ..b.md) inside repository without false outside-of-repo classification. - discover_files: verifies case-exact paths before returning mapped references. - openssf-baseline.toml: added docs/ARCHITECTURE.md and docs/API.md variants to discover and files, and updated files_to_include in OSPS-SA-01.01 and OSPS-SA-02.01 to ["README.md", "$RESOLVED_FILES"]. - docs/architecture/framework-design.md: documented case-exact resolution and physical file deduplication in sections 3.2, 3.3, 3.5, and 11.1. - Unit and regression tests covering case-exact path validation, deduplication, broken symlink rejection, slash-containing max_depth entries, outside path recording, and ..b.md filenames. Fixes darnitdevorg#645 Assisted-by: Claude:claude-3-5-sonnet Assisted-by: Gemini:gemini-2.5-pro Signed-off-by: Paradox <159635503+i-am-paradox@users.noreply.github.com>
1490162 to
b1d3c1b
Compare
|
Thanks @Marc-cn! Added the |
Marc-cn
left a comment
There was a problem hiding this comment.
Re-approved on b1d3c1b (trailers added, rebased on main, same change Mike verified on both volume types). On the VM against main: tests/darnit/sieve + tests/darnit_baseline + tests/darnit/core 2146 passed, ruff and validate_sync clean.
Summary
Fixes #645.
On case-insensitive filesystems (such as macOS APFS and Windows NTFS),
os.path.exists()andopen()match lowercase file probes against uppercase files in the repository. As reported in #645, this causes sieve handlers to:docs/architecture.mdwhen onlydocs/ARCHITECTURE.mdexists).llm_eval_handlerandllm_extract_handlerwhenfiles_to_includeorfilesspecify both case variants (such asREADME.mdandreadme.md).Key Changes
resolve_case_exact_path(path, root=None)indarnit.core.utilsto traverse each path component and verify it exists verbatim in actual directory entries (os.listdir), ensuring exact-case matching across all platforms.dir_entries(dir_path)keyed byst_mtime_ns._recheck.get_file_identity(full_path)returning(st.st_dev, st.st_ino)for physical file deduplication across paths, symlinks, or aliases.file_exists_handler: verifies exact case for globs,max_depthwalks, and direct path lookups while filtering broken symlinks and preserving repository containment._resolve_regex_files&_regex_exclude_evidence: ensures exact case matches, filters broken symlinks, and deduplicates by physical file identity.llm_eval_handler&llm_extract_handler: enforces case-exact paths and deduplicates opened files before adding tofile_contents.discover_files: verifies case-exact paths before returning mapped references.openssf-baseline.toml: updatedOSPS-SA-01.01andOSPS-SA-02.01to probedocs/ARCHITECTURE.mdanddocs/API.md(and.rstvariants) indiscoverandfiles.tests/darnit/core/test_utils.pyforresolve_case_exact_pathandget_file_identity.tests/darnit/sieve/test_builtin_handlers.pyforfile_exists_handler,regex_handler, depth-limited walks, andllm_eval_handlercase rejection & deduplication.test_builtin_handlers.pyandtest_discovery.pyusePath(...)andos.path.normpathfor cross-platform compatibility on Windows (\) and POSIX (/).Type of Change
Framework Changes Checklist
If this PR modifies the darnit framework (
packages/darnit/):docs/architecture/framework-design.md) if behavior changeduv run python scripts/validate_sync.py --verboseand it passesControl/TOML Changes Checklist
If this PR modifies controls or TOML configuration:
Testing
uv run pytest tests/ -v)uv run ruff check .)All unit and regression tests pass locally, and
scripts/validate_sync.py --verbosesucceeds. Path assertions usePath(...)andos.path.normpathto hold across both Windows and Linux/macOS.AI assistance
Assisted-by: Claude & Gemini for cross-platform reproduction analysis, path normalization investigation, and test fixture construction. All implementations, tests, and diffs were independently reviewed, executed, and verified locally.
Additional Notes
Addresses review feedback from @Marc-cn:
test_nested_case_exact_rejectionandtest_glob_case_exact_matchintest_builtin_handlers.pyto compare withPath(...)to handle Windows path separators (\).test_case_mismatch_not_discoveredintest_discovery.pyto useos.path.normpathfor Windows compatibility.