Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,7 @@ For background log or metrics sinks, use explicit writer-entry/release signals a
- Detailed database-diff coverage must keep record materialization within the requested JSON budget plus at most one candidate record, stop only at complete UTF-8 record boundaries, reject cursors after the deterministic difference sequence changes, and withhold continuations that cannot advance. Assert recovery guidance for zero-limit and first-record-too-large pages, suppress unusable cursor/replay metadata in embedded import dry-run comparisons, and preserve valid `--offset` continuation for non-detailed JSON and detailed text output.
- Keep the `files` / `map` `--exclude-tests` cross-command invariant in one seeded fixture: compare `map.file_count` with the parsed `files` row count for both the implicit production-source preset and an explicit `--path`.
- JSON failure-contract tests must assert that stdout contains one parseable versioned error object, stderr is empty, the documented exit code is preserved, and `status` / `error_code` identify the failure.
- Structural search-guard coverage must pair accepted evidence with unsafe controls in the same focused fixture. For whole-file reads, keep same-path size/control guards and resolved awaited bounded writers alongside unguarded reads, unrelated or reassigned paths, conditional size sources, inverted checks, unawaited helpers, out-of-order named arguments, and helpers in non-dominating switch sections. For traversal, pass inline, local, parameter, same-type, and reordered named `EnumerationOptions` to the actual `Directory.Enumerate*` call while retaining missing, unrelated, and shadowed-option controls. Assert accepted and rejected decision/reason/relationship/subject/container/evidence-path metadata rather than relying on source-line proximity.
- Auth-token recipe ranking coverage must pair high-signal credential fixtures with comment, regex-definition, and structural `CancellationToken` / syntax-token decoys, then assert explicit top-N precision for the bearer/authorization, GitHub/API/access-token, and token-secret child queries.
- `inspect` file-coordinate coverage must prove that an exact indexed positional path wins over symbol lookup and that missing paths or out-of-range lines fail before nearest-symbol resolution.
- For bounded NDJSON streams, include plain and recipe/audit search paths, parse the final `terminal_record`, verify authoritative-versus-lower-bound totals (including per-query and total-limit omissions) and selection reasons/counts before result-limit trimming, count UTF-8 bytes including newlines, and assert partial exit `11` plus the explicit `--allow-partial` exit-`0` opt-in. Also assert that capped profile/verbose/envelope combinations fail before stdout.
Expand Down Expand Up @@ -1828,6 +1829,7 @@ background の log / metrics sink は、sleep や狭い stopwatch 閾値では
- database diff の category coverage では、semantic に同等な1組の database を、volatile な timestamp / duration / mode / byte counter の drift、readiness/provenance drift、既定の semantic mode、`--data-only`、`--include-telemetry` で再利用する。判定対象に含む mode と除外する mode の両方で stable な category / reason code を検証し、content、graph、schema、legacy metadata の assertion は最も近い既存 fixture に維持する。
- `files` / `map` の `--exclude-tests` に関する cross-command invariant は、1 つの seed 済み fixture で維持する。暗黙の本番ソース preset と明示的な `--path` の両方について、`map.file_count` と parse 済み `files` row 数を比較する。
- JSON failure contract のテストでは、stdout に parse 可能な version 付き error object が 1 件だけあること、stderr が空であること、documented exit code が維持されること、`status` / `error_code` が失敗を識別することを検証する。
- 構造的な search guard の coverage では、同じ focused fixture 内で採用される evidence と unsafe control を対にしてください。whole-file read では、同一 path の size / control guard と解決済みかつ await 済みの bounded writer に加え、unguarded read、無関係または再代入された path、条件付き size source、反転 check、await されない helper、順序を入れ替えた named argument、支配しない switch section 内の helper を維持します。traversal では inline、local、parameter、同じ型、および順序を入れ替えた named `EnumerationOptions` を実際の `Directory.Enumerate*` 呼び出しへ渡し、options 未指定/無関係/shadowing の control も残します。source line の近接性ではなく、採用/棄却の decision、reason、relationship、subject、container、evidence path metadata を検証してください。
- auth-token recipe の ranking coverage では、高 signal な credential fixture と comment、regex 定義、構造的な `CancellationToken` / syntax-token decoy を対にし、bearer / authorization、GitHub / API / access-token、token-secret の各 child query について明示的な top-N precision を検証する。
- `inspect` の file-coordinate coverage では、indexed positional path の完全一致が symbol lookup より優先されることと、path 未検出・line 範囲外が nearest-symbol 解決前に失敗することを検証する。
- 上限付き NDJSON stream は plain search と recipe / audit search の両経路を含め、最後の `terminal_record` を解析し、query ごとおよび total-limit の省略を含む authoritative / lower-bound の総件数、result-limit 適用前の selection 理由 / 件数、改行を含む UTF-8 byte 数、partial 終了コード `11`、明示的な `--allow-partial` による終了コード `0` の opt-in を検証する。上限付き profile / verbose / envelope の組み合わせが stdout 前に失敗することも確認する。
Expand Down
21 changes: 18 additions & 3 deletions USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1377,8 +1377,16 @@ limit before/after checks to the same source line and the primary match column
ordering instead of nearby lines. JSON search results include
`guard_evidence` for matched guards and `guard_checks` for each guard evaluated
on a returned match. Guard evidence includes the guard name, pattern,
before/after relationship, scope (`window` or `same_line`), 1-based span,
origin category, and source line.
before/after relationship, scope (`window`, `same_line`, or the recipe-only
`container` scope), 1-based span, origin category, and source line. Built-in
whole-file-read and filesystem-traversal recipes use bounded C# structural
checks instead of line proximity: they correlate the same path through a
size/control guard or resolved bounded writer, and resolve the
`EnumerationOptions` value actually passed to `Directory.Enumerate*`.
Structural `guard_evidence` also reports `decision`, `reason`, `subject`,
`container`, and `evidence_path`; `guard_checks[].rejected_evidence` explains
why unrelated paths, inverted checks, unawaited helpers, or missing/unrelated
options were not accepted as guards.
Each `guard_checks[]` entry includes a compact pass/fail summary.
Guarded searches inspect a bounded candidate set before pagination; if a guarded
query is too broad to satisfy the requested page within that budget, CLI and MCP
Expand Down Expand Up @@ -4750,7 +4758,14 @@ guard-aware search は primary の `search` 一致を近傍の literal guard で
before / after を評価します。JSON の検索結果には
一致した guard の `guard_evidence` と、返却された一致に対して評価した各 guard の
`guard_checks` が含まれます。guard evidence には guard 名、pattern、before/after の関係、
scope(`window` または `same_line`)、1-based span、origin category、ソース行、簡潔な pass/fail summary が入ります。
scope(`window`、`same_line`、または recipe 専用の `container`)、1-based span、
origin category、ソース行、簡潔な pass/fail summary が入ります。組み込みの whole-file-read と
filesystem-traversal recipe は行の近接性ではなく、上限付きの C# 構造判定を使います。同じ path の
size / control guard または解決済み bounded writer を関連付け、`Directory.Enumerate*` に実際に
渡された `EnumerationOptions` 値を解決します。構造的な `guard_evidence` はさらに
`decision`、`reason`、`subject`、`container`、`evidence_path` を返し、
`guard_checks[].rejected_evidence` は無関係な path、反転した条件、await されない helper、
未指定または無関係な options が guard として採用されなかった理由を説明します。
guard filter を使う検索は pagination 前に上限付きの候補集合だけを調べます。その budget 内で
要求ページを満たせないほど query が広い場合、CLI/MCP は validation error を返します。
このエラーには guard budget、sampled candidate files / languages、`--count` / `--count-by`
Expand Down
25 changes: 25 additions & 0 deletions changelog.d/unreleased/4912.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
category: fixed
issues:
- 4912
affected:
- src/CodeIndex/Models/QueryResults.cs
- src/CodeIndex/Database/DbSearchReader.cs
- src/CodeIndex/Database/DbSearchReader.StructuralGuards.cs
- src/CodeIndex/Cli/SearchAuditRecipes.cs
- src/CodeIndex/Cli/QueryCommandRunner.ArgParsing.cs
- src/CodeIndex/Cli/QueryCommandRunner.ResultEnvelopes.cs
- src/CodeIndex/Cli/QueryCommandRunner.SearchRecipes.cs
- tests/CodeIndex.Tests/QueryCommandRunnerSearchGuardTests.cs
- tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs
- USER_GUIDE.md
- TESTING_GUIDE.md
---

## English

- **Safety-audit guards now follow structural evidence instead of fixed line proximity (#4912)** — whole-file-read recipes accept same-path size/control guards and resolved bounded-writer helpers, while filesystem-traversal recipes resolve the `EnumerationOptions` value actually passed by the call. Accepted and rejected candidates now include decision, reason, relationship, subject, container, and evidence-path metadata, so unrelated variables, inverted checks, missing options, and unawaited helpers remain actionable findings.

## 日本語

- **安全監査のガード判定が固定行の近接性ではなく構造的証拠を追跡するようになりました (#4912)** — whole-file-read レシピは同一路径のサイズ/制御ガードと解決済みの境界付き writer helper を採用し、filesystem-traversal レシピは呼び出しに実際に渡された `EnumerationOptions` 値を解決します。採用/棄却候補には decision、reason、relationship、subject、container、evidence path のメタデータが含まれ、無関係な変数、反転条件、options 未指定、await されない helper は引き続き対応対象として検出されます。
7 changes: 6 additions & 1 deletion src/CodeIndex/Cli/QueryCommandRunner.ArgParsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ private static bool TryNormalizeSearchGuardScope(string value, out SearchGuardSc
}

private static string FormatSearchGuardScope(SearchGuardScope scope)
=> scope == SearchGuardScope.SameLine ? "same-line" : "window";
=> scope switch
{
SearchGuardScope.SameLine => "same-line",
SearchGuardScope.Container => "container",
_ => "window",
};

public static QueryCommandOptions ParseArgs(
string[] args,
Expand Down
7 changes: 7 additions & 0 deletions src/CodeIndex/Cli/QueryCommandRunner.ResultEnvelopes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,13 @@ private static JsonArray BuildSearchGuardFiltersJson(IReadOnlyList<SearchGuardFi
};
if (filter.Scope.HasValue)
item["scope"] = FormatSearchGuardScope(filter.Scope.Value);
if (filter.EvidenceKind != SearchGuardEvidenceKind.Text)
item["evidence_kind"] = filter.EvidenceKind switch
{
SearchGuardEvidenceKind.CSharpBoundedFileRead => "csharp_bounded_file_read",
SearchGuardEvidenceKind.CSharpEnumerationOptions => "csharp_enumeration_options",
_ => "text",
};

filters.Add(item);
}
Expand Down
14 changes: 13 additions & 1 deletion src/CodeIndex/Cli/QueryCommandRunner.SearchRecipes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3891,7 +3891,19 @@ private static List<SearchRecipeGuardFilterJsonResult> ToSearchRecipeGuardFilter
filter.Direction == SearchGuardDirection.Before ? "before" : "after",
filter.Query,
BuildSearchGuardReplayOptionName(filter),
FormatSearchGuardFilterScope(filter)))
filter.Scope switch
{
SearchGuardScope.Window => "window",
SearchGuardScope.SameLine => "same_line",
SearchGuardScope.Container => "container",
_ => null,
},
filter.EvidenceKind switch
{
SearchGuardEvidenceKind.CSharpBoundedFileRead => "csharp_bounded_file_read",
SearchGuardEvidenceKind.CSharpEnumerationOptions => "csharp_enumeration_options",
_ => null,
}))
.ToList();

}
51 changes: 39 additions & 12 deletions src/CodeIndex/Cli/SearchAuditRecipes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,14 +211,14 @@ internal static class SearchAuditRecipes
"Classify the timestamp boundary first. Persisted and machine-facing values need explicit UTC or offset semantics; cache expiry must compare like-with-like clocks; elapsed-time and timeout logic should use monotonic duration primitives rather than DateTime wall-clock subtraction.");
private static readonly SearchRecipeClassifierJsonResult GuardEvidenceClassifier = new(
"guard_evidence",
"Classifies whether nearby guard checks explain why a risky API call is already bounded, filtered, or intentionally rejected.",
"Classifies whether guard checks structurally explain why a risky API call is already bounded, filtered, or intentionally rejected.",
[
new("bounded_positive_evidence", "A required guard appears near the primary match.", "Use guard_evidence and guard_checks to decide whether the hit is already bounded."),
new("missing_guard", "No required guard was found near the primary match.", "Prioritize review when the query describes an API that needs bounds or policy."),
new("bounded_positive_evidence", "A required guard is structurally tied to the primary match.", "Use guard_evidence and guard_checks to decide whether the hit is already bounded."),
new("missing_guard", "No applicable required guard was found for the primary match.", "Prioritize review when the query describes an API that needs bounds or policy."),
new("reject_guard_excluded", "A reject guard intentionally removed an otherwise noisy hit.", "Use --show-excluded or a narrower child query when auditing recipe precision.")
],
["guard_filters", "guard_evidence", "guard_checks", "risk_evidence"],
"Guard evidence is query-local context, not proof of safety; verify that the guard applies to the matched operation and not an unrelated nearby call.");
"Guard evidence is diagnostic context, not proof of safety; verify accepted/rejected reasons, subject, container, relationship, and evidence path for the matched operation.");
private static readonly SearchRecipeClassifierJsonResult SecretOriginClassifier = new(
"secret_origin",
"Classifies token/auth hits by likely sensitive runtime material versus structural, SQL, protocol, docs, or placeholder text.",
Expand Down Expand Up @@ -570,7 +570,18 @@ private static SearchAuditRecipeQuery TimestampBoundaryQuery(
"File.ReadAllText",
"Find whole-file text reads that may need size caps, sharing policy, or streaming alternatives.",
["audit", "performance"],
"False positives include bounded test fixtures and small files guarded by explicit size checks."),
"False positives include bounded test fixtures and small files guarded by explicit size checks.")
{
GuardFilters =
[
new(
SearchGuardRole.Reject,
SearchGuardDirection.Before,
"bounded-file-read",
SearchGuardScope.Container,
SearchGuardEvidenceKind.CSharpBoundedFileRead)
],
},
new(
"file-read-all-bytes",
"File.ReadAllBytes",
Expand Down Expand Up @@ -2719,19 +2730,23 @@ private static SearchAuditRecipeQuery TimestampBoundaryQuery(
new(
"enumerate-without-options",
"Directory.Enumerate",
"Find direct Directory.Enumerate* calls that do not have nearby EnumerationOptions evidence and may need traversal policy review.",
"Find direct Directory.Enumerate* calls that do not pass structurally resolved EnumerationOptions evidence and may need traversal policy review.",
["audit", "performance", "security"],
"False positives include known-small directories, already-budgeted traversal helpers, and wrappers that enforce cancellation or reparse-point policy.")
{
RiskEvidence =
[
"risk: direct Directory.Enumerate* calls without nearby EnumerationOptions can inherit default recursion, inaccessible-path, and reparse-point behavior.",
"positive: known-small directories, cancellation/budget checks, and shared traversal wrappers can explain intentional direct enumeration."
"risk: direct Directory.Enumerate* calls without a resolved EnumerationOptions argument can inherit default recursion, inaccessible-path, and reparse-point behavior.",
"positive: an inline, local, parameter, or same-container EnumerationOptions value passed to that call explains intentional direct enumeration."
],
GuardFilters =
[
new(SearchGuardRole.Reject, SearchGuardDirection.Before, "EnumerationOptions"),
new(SearchGuardRole.Reject, SearchGuardDirection.After, "EnumerationOptions")
new(
SearchGuardRole.Reject,
SearchGuardDirection.Before,
"configured-enumeration-options",
SearchGuardScope.Container,
SearchGuardEvidenceKind.CSharpEnumerationOptions)
],
MatchOrigins = ["code"],
},
Expand Down Expand Up @@ -3534,7 +3549,16 @@ private static SearchAuditRecipeQuery TimestampBoundaryQuery(
RiskEvidence =
[
"risk: whole-file text reads can materialize unbounded input without sharing or size policy.",
"positive: nearby length checks, BoundedFile helpers, or tiny trusted files can make a hit intentional."
"positive: same-path size/control checks, resolved bounded-writer helpers, or tiny trusted files can make a hit intentional."
],
GuardFilters =
[
new(
SearchGuardRole.Reject,
SearchGuardDirection.Before,
"bounded-file-read",
SearchGuardScope.Container,
SearchGuardEvidenceKind.CSharpBoundedFileRead)
],
},
new(
Expand Down Expand Up @@ -4415,7 +4439,10 @@ internal sealed record SearchRecipeGuardFilterJsonResult(
[property: JsonPropertyName("option")] string Option,
[property: JsonPropertyName("scope")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? Scope);
string? Scope,
[property: JsonPropertyName("evidence_kind")]
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? EvidenceKind);

internal sealed record SearchRecipeClassifierJsonResult(
[property: JsonPropertyName("name")] string Name,
Expand Down
Loading
Loading