Skip to content

A1: VersionIntent and constraint-aware package threat assessment#151

Open
sheeki03 wants to merge 16 commits into
mainfrom
security/a1-package-requirement-model
Open

A1: VersionIntent and constraint-aware package threat assessment#151
sheeki03 wants to merge 16 commits into
mainfrom
security/a1-package-requirement-model

Conversation

@sheeki03

@sheeki03 sheeki03 commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Part of the artifact-firewall hardening train (Stack A, PR 1 of 4). Bases on main. The rest of Stack A (A2 to A4) and Stack B will stack on this branch.

What

Replace the lossy version: Option<String> on package references with a VersionIntent enum (Unspecified / Exact / Constraint{raw, parsed} / Resolved) at both the command-path PackageRef and the ecosystem dependency model, so an unpinned or constrained install is no longer flattened to "no version" and silently treated as unpinned-clean. An unparsed or only partially understood constraint is preserved verbatim and treated as unresolved, never an exact pin.

Add a constraint-aware, serializable assess_package to the threat DB returning a tagged PackageThreatAssessment (NoRecord / ExactMatch / ConstraintIntersectsAffected / ConstraintExcludesAffected / Unresolved), built from wire strings (ThreatMatchSummary) since neither ThreatMatch nor ThreatSource derive Serialize. check_package stays as a shim. An all-versions record hard-matches even when Unspecified; ConstraintExcludesAffected is returned only on proven exclusion (whole requirement parsed, every affected version parsed, all evaluate false); any unsupported PEP 440 form (marker, epoch, local, ~=, ===, wildcard, parse failure) becomes Unresolved. PEP 440 parsing is PyPI only for now; other ecosystems stay unresolved.

New Medium / Warn RuleId ThreatUnresolvedMaliciousPackage, fired for the Unresolved and ConstraintIntersectsAffected paths on both the command path and the manifest/installed path. It does not short-circuit other signals: a co-present typosquat still fires, and only an ExactMatch suppresses weaker name-similarity findings.

Introduce escalation::finalize_static_verdict and use it in ecosystem_scan, which previously built its verdict via Verdict::from_findings and so ignored action_overrides. It applies severity overrides, action derivation, action overrides, then paranoia filtering without downgrading an override-forced Block.

Notes

  • Tests for the malicious-package paths build throwaway in-memory signed DBs rather than regenerating the shared test-threatdb.dat, because the generator also rotates the production Ed25519 verify key.
  • The new threat fixtures use existing hermetic test-threatdb.dat content (synthetic evil-package / reacct), not real package names.

Gate

fmt clean, clippy -D warnings clean, full workspace builds, 2961 tirith-core tests pass. The known parallel-env-race flakes (checkpoint-restore, clipboard-sidecar, url_validate documentation-range) pass single-threaded and touch no A1 code.

Stack

A2 (typed scan outcomes, coverage, file classification) will base on this branch.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a Threat Intel Medium warning: threat_unresolved_malicious_package when a malicious package name is known but the requested version is unpinned or overlaps affected versions, with remediation guidance.
  • Bug Fixes

    • Made threat assessment constraint-aware, distinguishing confirmed malicious vs unresolved cases.
    • Improved verdict handling so block overrides remain consistent after filtering.
    • Excluded this rule from injection-seed downgrade flows.
    • Updated per-package JSON to report the analyzed version intent.
  • Refactor

    • Introduced lossless version-intent handling across scans and installs.
  • Tests

    • Extended golden fixtures and added new threat-intel unresolved fixtures.

Greptile Summary

This PR replaces the lossy version: Option<String> on package references with a VersionIntent enum (Unspecified / Exact / Constraint / Resolved), adds a constraint-aware assess_package to the threat DB returning a tagged PackageThreatAssessment, and introduces finalize_static_verdict to ensure ecosystem_scan honours action_overrides and paranoia policy levers that the bare Verdict::from_findings constructor ignored.

  • VersionIntent + PEP 440 constraint evaluator (version_intent.rs): lossless representation of pip specifiers, Cargo caret requirements, npm X-ranges, and Gem version strings; conservatively rejects unsupported forms (markers, epochs, ~=, wildcards) as Unresolved.
  • assess_package / merge_assessments (threatdb.rs): constraint-aware assessment returning NoRecord / ExactMatch / ConstraintIntersectsAffected / ConstraintExcludesAffected / Unresolved; raw-token exact fallback handles non-semver identifiers (Docker digests, dist-tags); ConstraintExcludesAffected requires proven exclusion of every affected version.
  • finalize_static_verdict (escalation.rs) + manifest parsers (ecosystem_scan.rs): all manifest paths now carry version intent; Python python_requirement_spec strips extras brackets and inline comments; npm_partial_xrange uses checked_add to prevent overflow; ecosystem_scan now applies action and severity overrides via the new finalizer.

Confidence Score: 4/5

Safe to merge with one known cosmetic gap: the ecosystem-scan path's warning for a constraint that provably overlaps malicious versions uses the same imprecise 'could not be resolved' message as a fully-unresolvable constraint, where the command path correctly says 'overlaps the affected versions'. No security bypasses or missed detections were found.

The new assess_package logic, the VersionIntent constructors, python_requirement_spec inline-comment stripping, npm_partial_xrange overflow guard, and finalize_static_verdict are all correct and well-tested. The one remaining gap — unresolved_dependency_finding emitting a generic message for ConstraintIntersectsAffected while the command path generates a more specific one — is a UX inconsistency, not a missed detection or false negative. Prior-cycle findings (gem CLI from_explicit_version instead of from_gem_version, Gemfile multi-constraint second-spec dropped, Poetry version specs as Unspecified) are acknowledged deferred work and stay conservative (warn rather than silently pass).

crates/tirith-core/src/ecosystem_scan.rs — the unresolved_dependency_finding function and the findings_for block that calls it for both ConstraintIntersectsAffected and Unresolved.

Important Files Changed

Filename Overview
crates/tirith-core/src/version_intent.rs New file introducing VersionIntent enum and PEP 440 constraint parser; handles edge cases (local versions, markers, wildcards, overflow) conservatively and is well-tested.
crates/tirith-core/src/threatdb.rs Adds assess_package / assess_package_self with constraint-aware assessment and merge_assessments for supplemental overlay; all precedence rules and edge cases (empty-versions, local-version base-match, raw-token fallback) are well-tested.
crates/tirith-core/src/ecosystem_scan.rs Extensive manifest-parser upgrades carrying VersionIntent through every ecosystem; python_requirement_spec correctly strips extras and inline comments; npm_partial_xrange uses checked_add for overflow safety. Minor: unresolved_dependency_finding uses a generic 'could not be resolved' message for ConstraintIntersectsAffected where a more specific description is warranted.
crates/tirith-core/src/rules/threatintel.rs Command-path assessment migrated to assess_package; correct ecosystem-specific helpers used (e.g., from_cargo_version, from_pep440_specifier). Gem CLI --version flag still calls from_explicit_version rather than from_gem_version, which was flagged in a previous review cycle.
crates/tirith-core/src/escalation.rs New finalize_static_verdict closes the gap where ecosystem_scan ignored action_overrides; paranoia never downgrades an override-forced Block; causal findings are re-exposed post-paranoia so a restored Block still has an explanation. Four dedicated tests cover the critical code paths.
crates/tirith-core/src/install_txn.rs Docker/Go version parsing migrated to VersionIntent; OSV enrichment correctly restricts effective_version to exact_version() only, preventing range text from being sent as a literal version to the OSV API.
crates/tirith-core/src/threatdb_api.rs Uses exact_version() instead of version.clone() for the OSV lookup, correctly preventing constraint text from being sent as a literal version string.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Package reference\n(name + raw version string)"] --> B["VersionIntent\nconstructor"]
    B --> |"pip ==1.2.3"| C["Exact('1.2.3')"]
    B --> |"pip >=1.0,<2.0\n(parsed PEP 440)"| D["Constraint{parsed:Some}"]
    B --> |"cargo ^1.0\n(unparsed)"| E["Constraint{parsed:None}"]
    B --> |"pip install foo\n(no version)"| F["Unspecified"]
    B --> |"lockfile pin"| G["Resolved('1.2.3')"]
    C --> H["assess_package_self"]
    D --> H
    E --> H
    F --> H
    G --> H
    H --> |"all_versions_malicious"| I["ExactMatch → Critical/Block"]
    H --> |"Exact/Resolved matches DB"| I
    H --> |"Exact/Resolved misses DB"| J["NoRecord → no finding"]
    H --> |"Unspecified"| K["Unresolved → Medium/Warn"]
    H --> |"Constraint parsed,\noverlaps affected"| L["ConstraintIntersects → Medium/Warn"]
    H --> |"Constraint parsed,\nexcludes all affected"| M["ConstraintExcludes → no finding"]
    H --> |"Constraint unparsed,\nraw token literal match"| I
    H --> |"Constraint unparsed,\nno literal match"| K
    I --> N["finalize_static_verdict\n(overrides + paranoia)"]
    K --> N
    L --> N
    N --> O["EcosystemScanReport / InstallVerdict"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["Package reference\n(name + raw version string)"] --> B["VersionIntent\nconstructor"]
    B --> |"pip ==1.2.3"| C["Exact('1.2.3')"]
    B --> |"pip >=1.0,<2.0\n(parsed PEP 440)"| D["Constraint{parsed:Some}"]
    B --> |"cargo ^1.0\n(unparsed)"| E["Constraint{parsed:None}"]
    B --> |"pip install foo\n(no version)"| F["Unspecified"]
    B --> |"lockfile pin"| G["Resolved('1.2.3')"]
    C --> H["assess_package_self"]
    D --> H
    E --> H
    F --> H
    G --> H
    H --> |"all_versions_malicious"| I["ExactMatch → Critical/Block"]
    H --> |"Exact/Resolved matches DB"| I
    H --> |"Exact/Resolved misses DB"| J["NoRecord → no finding"]
    H --> |"Unspecified"| K["Unresolved → Medium/Warn"]
    H --> |"Constraint parsed,\noverlaps affected"| L["ConstraintIntersects → Medium/Warn"]
    H --> |"Constraint parsed,\nexcludes all affected"| M["ConstraintExcludes → no finding"]
    H --> |"Constraint unparsed,\nraw token literal match"| I
    H --> |"Constraint unparsed,\nno literal match"| K
    I --> N["finalize_static_verdict\n(overrides + paranoia)"]
    K --> N
    L --> N
    N --> O["EcosystemScanReport / InstallVerdict"]
Loading

Comments Outside Diff (3)

  1. crates/tirith-core/src/threatdb.rs, line 853-860 (link)

    P1 Non-semver exact strings silently downgraded from ExactMatch to Unresolved

    The command path now calls assess_package instead of check_package. For version strings that looks_like_plain_version rejects — Docker digests (sha256:abcdef…), the implicit-"latest" token from parse_docker_specs/parse_go_specs, and any other non-semver specifier — from_explicit_version produces Constraint { raw, parsed: None }. This arm returns Unresolved { ConstraintUnsupported } without attempting a verbatim string comparison against rec.versions.

    The old check_package path used rv == version directly. For a threat DB record that is not all_versions_malicious and whose affected version strings are non-semver (e.g., a specific Docker tag "3.15.0-alpine" or a digest), the assessment silently changes from ExactMatchThreatMaliciousPackage (Block/Critical) to UnresolvedThreatUnresolvedMaliciousPackage (Warn/Medium).

    A minimal fix is to do a verbatim fallback before the ConstraintUnsupported bail-out:

    VersionIntent::Constraint { parsed, raw } => {
        // Fallback: verbatim match preserves pre-existing behaviour for
        // non-semver exact specifiers (Docker digests, dist-tags stored
        // literally in the DB).
        if rec.versions.iter().any(|rv| rv == raw) {
            return PackageThreatAssessment::ExactMatch(summary);
        }
        let Some(constraint) = parsed else {
            return PackageThreatAssessment::Unresolved {};
        };

    The existing check_package_shim_still_matches test validates the shim, not the new assess_package path, so a regression here would not be caught by the current test suite.

    Fix in Claude Code

  2. crates/tirith-core/src/version_intent.rs, line 1409-1424 (link)

    P2 from_explicit_version accepts pre-release tails as Exact but from_pep440_specifier("==...") does not

    looks_like_plain_version allows -/+ tails, so from_explicit_version("1.2.3-beta.1")Exact("1.2.3-beta.1") (string-matched against the DB). But from_pep440_specifier("==1.2.3-beta.1") fails ReleaseVersion::parse on the hyphen and falls through to Constraint { parsed: None }Unresolved in assess_package_self, meaning the exact string "1.2.3-beta.1" is never compared against DB records.

    If a malicious PyPI package's affected version is stored as "1.2.3-beta.1" (a normalized PEP 440 pre-release would be 1.2.3b1, but threat feeds occasionally store the pip-style form), a user running pip install pkg==1.2.3-beta.1 would get Unresolved (false-positive warn) when the version is not in the DB, or Unresolved instead of ExactMatch when it is — either way the string comparison is skipped. The verbatim fallback suggested on assess_package_self would close this gap at the same time.

    Fix in Claude Code

  3. crates/tirith-core/src/ecosystem_scan.rs, line 104-118 (link)

    P1 security Integer overflow in npm_partial_xrange — security bypass in release mode / panic in debug mode

    major + 1 and minor + 1 are plain u64 arithmetic. If a package.json contains a dependency version like "18446744073709551615" (a string that parses cleanly as a single numeric segment), npm_partial_xrange overflows:

    • Debug build: panics with "attempt to add with overflow" — attackers can crash tirith by planting a crafted manifest.
    • Release build (default, no overflow-checks): wraps to 0, producing ">=18446744073709551615.0.0,<0.0.0". <0.0.0 never matches any real version, so VersionConstraint::matches returns false for every affected version, intersecting.is_empty() is true, and assess_package_self returns ConstraintExcludesAffected — a proven-safe verdict — for a package that is actually malicious. The threat warning is completely suppressed.

    Use checked_add(1) and return None on overflow so the version falls back to an Exact pin (which performs a correct literal/numeric string compare against the DB) rather than generating a malformed exclusion constraint.

    Fix in Claude Code

Reviews (29): Last reviewed commit: "fix(ecosystem): strip inline `#` comment..." | Re-trigger Greptile

…assessment (A1)

Replace the lossy `version: Option<String>` on package references with a
`VersionIntent` enum (Unspecified / Exact / Constraint{raw, parsed} / Resolved)
at both sites (the command-path `PackageRef` and the ecosystem dependency
model), so an unpinned or constrained install is no longer flattened to "no
version" and silently treated as unpinned-clean. An unparsed or only partially
understood constraint is preserved verbatim and treated as unresolved, never as
an exact pin.

Add a constraint-aware, serializable `assess_package` to the threat DB returning
a tagged `PackageThreatAssessment` (NoRecord / ExactMatch / ConstraintIntersects
Affected / ConstraintExcludesAffected / Unresolved) built from wire strings
(`ThreatMatchSummary`), since neither `ThreatMatch` nor `ThreatSource` derive
Serialize. `check_package` is kept as a shim. An all-versions record hard-matches
even when Unspecified; `ConstraintExcludesAffected` is returned only on proven
exclusion (whole requirement parsed, every affected version parsed, all evaluate
false), and any unsupported PEP 440 form (marker, epoch, local, `~=`, `===`,
wildcard, parse failure) becomes Unresolved. Supplemental precedence: exact wins,
else unresolved with deduped affected versions, else intersect, else exclude,
else NoRecord, so an overlay can never silence a primary hit.

Add a new Medium/Warn RuleId `ThreatUnresolvedMaliciousPackage`, fired for the
Unresolved and ConstraintIntersectsAffected paths on both the command path
(`rules::threatintel::check`) and the manifest/installed path (`ecosystem_scan`).
It does not short-circuit other signals: a co-present typosquat still fires; only
an ExactMatch suppresses weaker name-similarity findings.

Introduce `escalation::finalize_static_verdict`, applying severity overrides,
action derivation, action overrides, then paranoia filtering without downgrading
an override-forced Block, and use it in `ecosystem_scan` (which previously built
its verdict via `Verdict::from_findings` and so ignored `action_overrides`).
`confidence_to_severity` is now `pub(crate)` so the scan path maps an ExactMatch
to the same severity as the command path. `DependencyAssessment` carries an
`Option<PackageThreatAssessment>` summary (skip_serializing_if), and
`DeclaredDependency.version` serializes as the original version string so the
JSON shape is unchanged.

A conservative PEP 440 release-segment evaluator lives in the new
`version_intent` module; a full solver is intentionally out of scope. Includes
assess_package unit cases per VersionIntent (including unparsed and `===` giving
Unresolved, excluding vs intersecting, all-versions matching Unspecified,
supplemental dedup), a `check_package` shim regression, threatintel.toml fixtures
(unpinned and range malicious warn; co-present typosquat still fires), an
ecosystem_scan installed-METADATA exact-match test, and finalize_static_verdict
tests for the action-override-honored and paranoia-no-downgrade behavior.
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 84e232db-332e-49f1-b152-3d0a36b0861a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a VersionIntent typed model replacing Option<String> for package versions, enabling lossless representation of exact pins, constraint/range expressions, and unspecified versions. Adds ThreatDb::assess_package for constraint-aware threat matching, a new ThreatUnresolvedMaliciousPackage rule emitted when a package name is known-malicious but the requested version cannot be confirmed non-overlapping, and a finalize_static_verdict helper. All parsers across npm, pip, cargo, gem, dotnet, maven, gradle, Docker, and Go are migrated to emit VersionIntent, and threat assessment is wired into both the threatintel command path and the ecosystem_scan manifest path.

Changes

Constraint-Aware Threat Detection

Layer / File(s) Summary
VersionIntent: type, constructors, constraint evaluator, and tests
crates/tirith-core/src/version_intent.rs, crates/tirith-core/src/lib.rs
Defines VersionIntent enum with Unspecified, Exact, Constraint, and Resolved variants; implements from_pep440_specifier, from_explicit_version, and from_cargo_version constructors; introduces VersionConstraint with conservative PEP 440 clause parsing (comma-separated AND, operators: ==, !=, <, <=, >, >=) and ReleaseVersion with trailing-zero numeric ordering; comprehensive unit test suite; exports as public crate module.
ThreatDb constraint-aware assessment types and API
crates/tirith-core/src/threatdb.rs
Adds ThreatMatchSummary, UnresolvedReason, and PackageThreatAssessment serde-serializable enum covering exact match, constraint-intersection, proven exclusion, and unresolved outcomes; implements assess_package / assess_package_self resolving version-intent semantics including trailing-zero/PEP 440 local handling, raw-token exact fallback, and unresolved reasons; adds merge_assessments combining primary and supplemental overlay with explicit precedence (exact > unresolved > intersects > excludes > no-record) and affected-version dedup; extended unit tests.
New RuleId, finalize_static_verdict, and unresolved finding builder
crates/tirith-core/src/verdict.rs, crates/tirith-core/src/escalation.rs, crates/tirith-core/src/rules/threatintel.rs
Adds RuleId::ThreatUnresolvedMaliciousPackage variant with doc comment; introduces finalize_static_verdict function applying per-rule severity_override, then Verdict::from_findings, then per-rule action_overrides (with block-upgrade snapshot logic), then paranoia filtering (restoring override-forced Block if paranoia would downgrade); adds unresolved_package_finding helper building Medium finding with constraint-text context; marks confidence_to_severity pub(crate); unit tests for all three.
threatintel.rs parser migration and check() refactor
crates/tirith-core/src/rules/threatintel.rs
Migrates PackageRef.version from Option<String> to VersionIntent across pip, npm, cargo, gem, dotnet, maven, gradle parsers; removes extract_pip_version; adds intent-classification helpers. Refactors check() to use db.assess_package and branch on PackageThreatAssessment: ExactMatch emits ThreatMaliciousPackage (High) and short-circuits; ConstraintIntersectsAffected and Unresolved emit new unresolved finding without short-circuiting so typosquat checks continue. Updates all parser unit tests to assert VersionIntent variants.
ecosystem_scan and install_txn parser migration to VersionIntent
crates/tirith-core/src/ecosystem_scan.rs, crates/tirith-core/src/install_txn.rs
Migrates DeclaredDependency.version from Option<String> to VersionIntent with serde-omit-when-unspecified across npm, python, rust, go, ruby manifest and all lockfile/installed-tree walkers; adds lock_version_intent helper mapping resolved versions to VersionIntent::Resolved; adds serde helpers for field omission; updates Docker and Go specs to emit VersionIntent; updates all parser and test expectations.
Ecosystem scan threat assessment integration and verdict assembly
crates/tirith-core/src/ecosystem_scan.rs
Adds threat_assessment: Option<PackageThreatAssessment> field to DependencyAssessment; populates it from db.assess_package during installed-tree walk; refactors findings_for to emit ExactMatch early-return with High "known malicious" and ConstraintIntersectsAffected/Unresolved emission of Medium unresolved finding while continuing name-based logic; switches verdict assembly from Verdict::from_findings to finalize_static_verdict tier 3; updates test helpers and adds installed-mode threat-DB tests with synthetic DB and METADATA.
Rule registration, callsite updates, and test fixtures
crates/tirith-core/src/scoring.rs, crates/tirith-core/src/mcp/output_filter.rs, crates/tirith-core/build.rs, crates/tirith-core/tests/golden_fixtures.rs, crates/tirith-core/assets/data/rule_explanations.toml, crates/tirith-core/src/engine.rs, crates/tirith-core/src/threatdb_api.rs, crates/tirith/src/cli/install.rs, tests/fixtures/threatintel.toml, tests/fixtures/ecosystem.toml
Registers ThreatUnresolvedMaliciousPackage in scoring threat-intel classification, output-filter injection-seed list, build.rs validation, golden fixtures, and rule-explanations TOML; updates engine.rs, threatdb_api.rs, and CLI install.rs callsites to as_version_str() for threat-db queries; adds threatintel.toml unpinned/range/combined fixtures; updates ecosystem.toml M6 fixture to use synthetic package name.

Sequence Diagram(s)

sequenceDiagram
    participant Parser as "Command/Manifest Parser"
    participant ThreatPath as "rules::threatintel / ecosystem_scan"
    participant ThreatDb as "ThreatDb"
    participant Escalation as "escalation::finalize_static_verdict"

    Parser->>ThreatPath: PackageRef / DeclaredDependency<br/>(version: VersionIntent)
    ThreatPath->>ThreatDb: assess_package(eco, name, &VersionIntent)
    ThreatDb-->>ThreatPath: PackageThreatAssessment
    alt ExactMatch
        ThreatPath->>ThreatPath: emit ThreatMaliciousPackage (High)<br/>short-circuit, skip typosquat
    else ConstraintIntersectsAffected or Unresolved
        ThreatPath->>ThreatPath: emit ThreatUnresolvedMaliciousPackage (Medium)<br/>continue with typosquat/similarity checks
    else NoRecord / ConstraintExcludesAffected
        ThreatPath->>ThreatPath: no package finding<br/>allow normal checks to proceed
    end
    ThreatPath->>Escalation: findings + policy + tier
    Escalation->>Escalation: apply severity_override<br/>apply action_overrides (snapshot block upgrade)<br/>paranoia filter (restore block if downgraded)
    Escalation-->>ThreatPath: Verdict
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐇 A version's a promise — now typed as Intent,
Exact? Constraint? Unspecified went.
The threat DB checks: does the constraint intersect?
Unresolved malicious? A Medium warning direct!
Parsers emit Intent from pip, npm, and more,
The rabbit secures packages—threat-safe to the core! 🔐

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the two main changes: introducing VersionIntent (a typed version model) and implementing constraint-aware package threat assessment.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread crates/tirith-core/src/ecosystem_scan.rs
…A1 review)

T1.9: assess_package_self compared the requested version to affected versions
by literal string, so an exact pin like ==1.4 missed a record listing 1.4.0.
It now also matches on numeric ReleaseVersion equality when both parse. A lone
==<ver> in from_pep440_specifier is Exact regardless of parse success, so a
prerelease pin (==1.0.0rc1) blocks instead of degrading to a constraint warn.

T3.4: cap ReleaseVersion::parse at 16 dot-separated segments to bound the
segment allocation on attacker-influenceable version strings.
@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/tirith-core/src/ecosystem_scan.rs (1)

423-428: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve written constraints before emitting Unspecified.

These parsers now drop constraints/pins and set VersionIntent::Unspecified; with assess_package, a safe pin such as malicious-pkg==9.9.9 or gem "x", "9.9.9" warns as unresolved against any version-specific malicious record. Preserve the raw spec into VersionIntent (PEP 440 parsing for PyPI; raw unsupported constraints for Gemfile) so users can clear warnings by pinning a non-affected version.

Also applies to: 463-477, 710-716

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/src/ecosystem_scan.rs` around lines 423 - 428, The
python_requirement_name function is only extracting the package name from
requirements.txt lines and discarding the version constraint information,
causing all dependencies to be marked with VersionIntent::Unspecified. Instead
of hardcoding VersionIntent::Unspecified in the DeclaredDependency creation,
extract the version constraint from the same line using PEP 440 parsing (or a
helper function) and populate the version field with the parsed constraint. This
allows the assess_package function to properly handle pinned versions like
malicious-pkg==9.9.9 and prevent false warnings when users pin non-affected
versions. Apply the same fix to the other locations mentioned (around lines
463-477 and 710-716) where similar parsing occurs for different package
managers.
🧹 Nitpick comments (2)
tests/fixtures/threatintel.toml (1)

72-105: ⚡ Quick win

Add a fixture that guards the non-semver exact-token path.

These new fixtures cover unresolved npm intent well, but they don’t lock the Docker/Go literal-token regression class (sha256:..., latest, prerelease-like literals) where exact malicious hits can be downgraded to unresolved. Add at least one fixture that expects threat_malicious_package (and forbids unresolved) for a version-keyed malicious record using a literal non-semver token.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/fixtures/threatintel.toml` around lines 72 - 105, Add a new fixture
block after the existing fixtures that tests a literal non-semver token (such as
a Docker sha256 hash, "latest", or prerelease-like literal) with a malicious
package record. The fixture should use an input command that includes this
non-semver literal token, set expected_action to "block", include
"threat_malicious_package" in the expected_rules array, and add
"threat_unresolved_malicious_package" to the forbidden_rules array to ensure the
exact malicious match is confirmed rather than being downgraded to unresolved.
This guards against regression where literal token paths incorrectly downgrade
exact malicious hits to unresolved status.
crates/tirith-core/src/scoring.rs (1)

83-83: ⚡ Quick win

Add a regression assertion for the new threat-intel rule classification.

The new classification is correct, but the nearby unit test should assert ThreatUnresolvedMaliciousPackage explicitly to lock this behavior.

Suggested test update
 fn is_threat_intel_rule_classifies_threat_family() {
     assert!(is_threat_intel_rule(RuleId::ThreatMaliciousPackage));
+    assert!(is_threat_intel_rule(
+        RuleId::ThreatUnresolvedMaliciousPackage
+    ));
     assert!(is_threat_intel_rule(RuleId::ThreatCisaKev));
     assert!(is_threat_intel_rule(RuleId::ThreatSafeBrowsing));
     assert!(!is_threat_intel_rule(RuleId::CurlPipeShell));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/src/scoring.rs` at line 83, Add a regression assertion in
the unit test near the RuleId::ThreatUnresolvedMaliciousPackage classification
to explicitly verify that this threat-intel rule is correctly classified. This
assertion should test the behavior of the new ThreatUnresolvedMaliciousPackage
rule to ensure its classification does not regress in future changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/tirith-core/src/ecosystem_scan.rs`:
- Around line 283-288: The from_explicit_version call is incorrectly classifying
plain version tokens (like "1.2.3", "1", "1.2") as exact version pins when they
should be treated as version ranges per ecosystem semantics (Cargo caret
requirements, npm X-Ranges). Replace the call to
VersionIntent::from_explicit_version(v) with a more conservative approach using
Constraint { raw, parsed: None } to preserve these declarations as unresolved
ranges, allowing threat assessment to properly match against versions that fall
within the declared range. Apply this same fix in two locations: around line 283
in the current snippet and also around lines 587-591 for Cargo dependencies.

In `@crates/tirith-core/src/escalation.rs`:
- Around line 327-332: The finalize_static_verdict function restores an
override-forced Block verdict after paranoia filtering, but it does not preserve
the findings that caused the override, leaving callers with a Block verdict that
lacks explanation. Modify finalize_static_verdict to retain the rule IDs
returned by apply_action_overrides, and after the call to
crate::engine::filter_findings_by_paranoia, re-add any findings whose rule IDs
match those override-causing rule IDs back into the verdict's findings. Mirror
the approach used in post_process_verdict to ensure consistency. Additionally,
add a regression test that verifies an overridden Low or Info severity finding
is preserved in the verdict even when default paranoia filtering would normally
remove it.

In `@crates/tirith-core/src/install_txn.rs`:
- Around line 645-653: The PackageRef construction for Docker ecosystem at the
version_token assignment and similar locations for Go ecosystem (around lines
708-710 and 730) route non-semver selectors like sha256 digests and "latest"
through VersionIntent::from_explicit_version, which classifies them as
unsupported constraints and causes true version-keyed malicious hits to be
downgraded to unresolved warnings instead of exact malicious blocks. Add a
verbatim exact-compare fallback in the assessment layer before returning
unsupported or unresolved intent, so that literal selectors for Docker and Go
ecosystems can still be matched exactly against malicious package versions even
when they don't conform to semver formatting.

In `@crates/tirith-core/src/rules/threatintel.rs`:
- Around line 751-812: The assess_package method needs to add a raw-version
fallback for non-semver version tokens before returning Unresolved. When a
version constraint cannot be parsed (parsed: None), and before returning
PackageThreatAssessment::Unresolved, explicitly check if the raw unparsed
version token matches any exact affected-version records in the threat
intelligence database. If a raw version match is found, return
PackageThreatAssessment::ExactMatch instead of downgrading to Unresolved. This
ensures that malicious packages with non-standard version formats maintain their
high-severity ThreatMaliciousPackage classification instead of being incorrectly
downgraded to a medium-severity unresolved warning.
- Around line 178-181: The `from_pep440_specifier` helper is incorrectly parsing
unsupported PEP 440 specifier forms (such as `===`, markers like `python_version
>= '3.11'`, epochs, and locals) as exact versions instead of rejecting them as
unresolved. Before passing the `rest` variable to `from_pep440_specifier`, add
validation logic to detect and reject these unsupported forms (check for the
presence of markers, triple-equals `===`, epochs using colons, and local version
segments using `+`), and return an unresolved intent with an appropriate warning
for these cases instead of allowing them to reach the exact version parsing fast
path. Additionally, add parser tests that verify marker requirements and
arbitrary-equality specifications are properly handled as unresolved rather than
being incorrectly converted to exact versions.

In `@crates/tirith-core/src/threatdb.rs`:
- Around line 1095-1101: When handling VersionIntent::Constraint with parsed:
None, do not immediately return Unresolved. Before returning Unresolved, check
if the raw value exactly matches any of the affected versions in the affected
collection. If an exact match is found, return an ExactMatch
PackageThreatAssessment instead. Only return Unresolved if the raw value does
not match any affected versions. Additionally, add a regression test that
verifies when the DB affected version and the request raw token are identical
unparsed values, the result is ExactMatch.

In `@crates/tirith-core/src/version_intent.rs`:
- Around line 97-101: The exact version parsing block that checks for the "=="
prefix is not rejecting environment markers (indicated by semicolons). Modify
the condition in the exact version check (the one that currently validates
`!ver.is_empty() && !ver.contains(',') && !ver.contains('*')`) to also reject
versions containing semicolons by adding `&& !ver.contains(';')`. This ensures
that marker-qualified exact pins like `==1.0.0;python_version<"3.9"` fall
through to the Constraint parsing path instead of creating a malformed Exact
version intent.

---

Outside diff comments:
In `@crates/tirith-core/src/ecosystem_scan.rs`:
- Around line 423-428: The python_requirement_name function is only extracting
the package name from requirements.txt lines and discarding the version
constraint information, causing all dependencies to be marked with
VersionIntent::Unspecified. Instead of hardcoding VersionIntent::Unspecified in
the DeclaredDependency creation, extract the version constraint from the same
line using PEP 440 parsing (or a helper function) and populate the version field
with the parsed constraint. This allows the assess_package function to properly
handle pinned versions like malicious-pkg==9.9.9 and prevent false warnings when
users pin non-affected versions. Apply the same fix to the other locations
mentioned (around lines 463-477 and 710-716) where similar parsing occurs for
different package managers.

---

Nitpick comments:
In `@crates/tirith-core/src/scoring.rs`:
- Line 83: Add a regression assertion in the unit test near the
RuleId::ThreatUnresolvedMaliciousPackage classification to explicitly verify
that this threat-intel rule is correctly classified. This assertion should test
the behavior of the new ThreatUnresolvedMaliciousPackage rule to ensure its
classification does not regress in future changes.

In `@tests/fixtures/threatintel.toml`:
- Around line 72-105: Add a new fixture block after the existing fixtures that
tests a literal non-semver token (such as a Docker sha256 hash, "latest", or
prerelease-like literal) with a malicious package record. The fixture should use
an input command that includes this non-semver literal token, set
expected_action to "block", include "threat_malicious_package" in the
expected_rules array, and add "threat_unresolved_malicious_package" to the
forbidden_rules array to ensure the exact malicious match is confirmed rather
than being downgraded to unresolved. This guards against regression where
literal token paths incorrectly downgrade exact malicious hits to unresolved
status.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: caca0c21-5f92-4762-8f37-a262904b5e69

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2f81f and 301b8a2.

📒 Files selected for processing (18)
  • crates/tirith-core/assets/data/rule_explanations.toml
  • crates/tirith-core/build.rs
  • crates/tirith-core/src/ecosystem_scan.rs
  • crates/tirith-core/src/engine.rs
  • crates/tirith-core/src/escalation.rs
  • crates/tirith-core/src/install_txn.rs
  • crates/tirith-core/src/lib.rs
  • crates/tirith-core/src/mcp/output_filter.rs
  • crates/tirith-core/src/rules/threatintel.rs
  • crates/tirith-core/src/scoring.rs
  • crates/tirith-core/src/threatdb.rs
  • crates/tirith-core/src/threatdb_api.rs
  • crates/tirith-core/src/verdict.rs
  • crates/tirith-core/src/version_intent.rs
  • crates/tirith-core/tests/golden_fixtures.rs
  • crates/tirith/src/cli/install.rs
  • tests/fixtures/ecosystem.toml
  • tests/fixtures/threatintel.toml

Comment thread crates/tirith-core/src/ecosystem_scan.rs
Comment thread crates/tirith-core/src/escalation.rs
Comment thread crates/tirith-core/src/install_txn.rs
Comment thread crates/tirith-core/src/rules/threatintel.rs
Comment thread crates/tirith-core/src/rules/threatintel.rs
Comment thread crates/tirith-core/src/threatdb.rs Outdated
Comment thread crates/tirith-core/src/version_intent.rs
…ent (A1 review)

- from_pep440_specifier: gate the `==` exact fast path on looks_like_plain_version
  so arbitrary-equality (`===`), environment markers, epochs, and wildcards fall
  through to an unresolved Constraint instead of a bogus Exact string.
- assess_package_self: raw-token exact fallback. A non-semver token (a Docker
  digest, a `latest` tag) preserved as Constraint{parsed:None} that literally
  equals an affected version is now an ExactMatch (malicious block), not a
  downgraded Unresolved warning.
- finalize_static_verdict: re-expose the override-causal finding after paranoia
  filtering, so a restored override-forced Block is never left without a cause
  (mirrors post_process_verdict).
- ecosystem_scan: capture the PEP 508 specifier for requirements.txt and PEP 621
  dependencies so a real pin is assessed instead of a spurious unresolved warning;
  classify npm partial versions (`1`, `1.2`) as X-ranges, not too-narrow exact pins.

Addresses CodeRabbit (version_intent/threatdb/threatintel/install_txn/escalation/
ecosystem_scan) and Greptile (pinned-dependency false positive) review findings.
@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Comment thread crates/tirith-core/src/ecosystem_scan.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/tirith-core/src/rules/threatintel.rs (1)

307-370: ⚠️ Potential issue | 🟠 Major

Treat cargo add name@version as a Cargo requirement constraint, not an exact pin.

cargo add serde@1.0.193 adds a caret requirement (equivalent to ^1.0.193), not an exact version. The current code uses the shared split_name_version path for both cargo install and cargo add, incorrectly modeling @version as Exact for both commands. This causes compatible affected versions to return NoRecord instead of unresolved findings. Keep cargo install --version exact, but model cargo add versions as unresolved constraints.

Proposed fix
+fn cargo_requirement_intent(ver: &str) -> VersionIntent {
+    let raw = ver.trim();
+    if raw.is_empty() {
+        VersionIntent::Unspecified
+    } else {
+        VersionIntent::Constraint {
+            raw: raw.to_string(),
+            parsed: None,
+        }
+    }
+}
+
 fn extract_cargo_packages(args: &[String], packages: &mut Vec<PackageRef>) {
     let mut iter = args.iter();
     let mut found_subcmd = false;
+    let mut cargo_add = false;
@@
     if !found_subcmd {
         if matches!(lower.as_str(), "install" | "add") {
+            cargo_add = lower == "add";
             found_subcmd = true;
         }
         continue;
@@
-                                last.version = VersionIntent::from_explicit_version(ver);
+                                last.version = if cargo_add {
+                                    cargo_requirement_intent(ver)
+                                } else {
+                                    VersionIntent::from_explicit_version(ver)
+                                };
                             }
                         }
                     }
@@
-        let (name, version) = split_name_version(arg, '@');
+        let (name, version) = if cargo_add {
+            match arg.split_once('@') {
+                Some((name, ver)) => (name, cargo_requirement_intent(ver)),
+                None => (arg.as_str(), VersionIntent::Unspecified),
+            }
+        } else {
+            split_name_version(arg, '@')
+        };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/src/rules/threatintel.rs` around lines 307 - 370, The
extract_cargo_packages function incorrectly treats `cargo add name@version` as
an exact version constraint when it should be treated as an unresolved caret
requirement. Track whether the current subcommand being processed is "add" or
"install" by maintaining the found_subcmd flag value separately. When creating a
PackageRef for a cargo add command, check if a version was extracted via
split_name_version and if so, set the version to an unresolved constraint
(VersionIntent::Unresolved) instead of using the explicit version from
split_name_version. Keep the existing logic for handling --version flags in
cargo install as exact versions. This distinction between the two commands
ensures that cargo add versions are properly modeled as caret constraints rather
than exact pins.
🧹 Nitpick comments (2)
crates/tirith-core/src/scoring.rs (1)

83-83: ⚡ Quick win

Add an explicit classification assertion for the new threat-intel RuleId.

is_threat_intel_rule_classifies_threat_family should include RuleId::ThreatUnresolvedMaliciousPackage to lock this behavior and prevent silent regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/src/scoring.rs` at line 83, The new
RuleId::ThreatUnresolvedMaliciousPackage needs to be explicitly handled in the
is_threat_intel_rule_classifies_threat_family function to ensure proper threat
classification behavior and prevent future regressions. Add a match arm for
RuleId::ThreatUnresolvedMaliciousPackage in the
is_threat_intel_rule_classifies_threat_family function that returns true,
alongside any other threat-intel rules that classify threat families.
tests/fixtures/threatintel.toml (1)

72-105: ⚡ Quick win

Add a fixture for non-semver explicit versions to guard the hot-path regression.

Please add a case like npm install evil-package@latest (or another non-PEP440/non-semver token) that must still produce the confirmed malicious-package rule when the threat DB contains the same raw affected version string. Current additions don’t lock this regression path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/fixtures/threatintel.toml` around lines 72 - 105, Add a new fixture to
the threatintel.toml file that tests the non-semver explicit version regression
path. Create a fixture with a name like
npm_install_explicit_nonsemantics_malicious_matches that uses input like "npm
install evil-package@latest" or another non-semver token, sets min_milestone to
1, context to "exec", expected_action to "block", and includes
threat_malicious_package in the expected_rules (not just
threat_unresolved_malicious_package). This ensures that when the threat database
contains the same raw affected version string, the confirmed exact-match
malicious package rule fires correctly for non-semver version specifiers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/tirith-core/src/ecosystem_scan.rs`:
- Around line 324-330: The code performs unchecked arithmetic operations with
`major + 1` and `minor + 1` in the match arms for version range parsing, which
can overflow when these values are u64::MAX, causing panics in checked builds or
incorrect behavior in release builds. For each match arm ([major] and [major,
minor]), add overflow checks before performing the increment operations using
checked_add or by comparing against u64::MAX, and return None to leave the spec
unresolved if an overflow would occur, otherwise proceed with the range
generation.
- Around line 490-508: The `python_requirement_spec` function is stripping the
environment marker (the part after `;`) before extracting the version specifier,
which causes marker-qualified requirements like `evil==1.0; python_version <
"3.9"` to lose their markers and be treated as unconditional exact pins. To fix
this, remove the line that strips the marker (the split on `;`) and instead
apply the specifier extraction logic directly to the full requirement line. This
will preserve the environment marker in the returned string, allowing
`from_pep440_specifier` to detect it via `looks_like_plain_version` and properly
treat marker-qualified pins as unresolved constraints rather than exact pins.

In `@crates/tirith-core/src/rules/threatintel.rs`:
- Around line 689-694: The UnresolvedKind::Unresolved branch is ignoring the
reason field, resulting in generic messages that don't accurately reflect
whether the issue is with the user's constraint or the threat feed's
affected-version strings. Instead of using static messages like "The constraint
could not be fully resolved", extract and use the UnresolvedReason to generate
more specific error messages that accurately convey the actual root cause of the
resolution failure. Apply this change to both the UnresolvedKind::Unresolved
match block and the similar code handling the other unresolved kind variants
mentioned in the also applies to reference.

In `@crates/tirith-core/src/threatdb.rs`:
- Around line 1113-1133: The issue is that when rec.versions is empty, the code
creates an empty parsed_affected vector, causing intersecting.is_empty() to be
true, which returns ConstraintExcludesAffected. This incorrectly treats missing
version metadata as proof that the constraint excludes all affected versions,
when it should instead indicate that there is insufficient data to determine
exclusion. Fix this by adding a check after the version parsing loop in the
section with parsed_affected: if parsed_affected is empty and rec.versions was
empty, return PackageThreatAssessment::Unresolved with an appropriate reason
(like AffectedVersionUnparsed or a new reason indicating missing data), before
reaching the ConstraintExcludesAffected return. Only return
ConstraintExcludesAffected if parsed_affected contains entries but none matched
the constraint. Additionally, add a regression test that builds a
version-specific malicious record with empty versions array and verifies it
returns Unresolved rather than ConstraintExcludesAffected.

In `@crates/tirith-core/src/version_intent.rs`:
- Around line 97-106: The `looks_like_plain_version` function currently accepts
the `+` character, which represents PEP 440 local versions (e.g.,
`==1.0+ubuntu1`). This causes the condition within the `strip_prefix("==")`
block to treat such versions as Exact, but the threat-DB literal/numeric
comparison cannot properly match base records, leading to silent failures
instead of the expected UNRESOLVED behavior. Modify `looks_like_plain_version`
to reject the `+` character alongside the other already-rejected special
characters (`;`, `!`, `*`, etc.), so that local-version pins fall through to the
Constraint path and correctly result in UNRESOLVED instead of creating bogus
Exact strings.
- Around line 151-161: The looks_like_plain_version function incorrectly accepts
malformed versions with empty segments like `1.`, `1..2`, and `1.+` as exact
pins. The current segment validation only rejects wildcard segments (x/X) but
does not check for empty segments. Change the validation logic from rejecting
segments that match x or X to accepting only non-empty segments that do not
match x or X by replacing the any() check with an all() check that ensures each
segment produced by splitting on ['.', '-', '+'] is both non-empty and not equal
to x or X.

---

Outside diff comments:
In `@crates/tirith-core/src/rules/threatintel.rs`:
- Around line 307-370: The extract_cargo_packages function incorrectly treats
`cargo add name@version` as an exact version constraint when it should be
treated as an unresolved caret requirement. Track whether the current subcommand
being processed is "add" or "install" by maintaining the found_subcmd flag value
separately. When creating a PackageRef for a cargo add command, check if a
version was extracted via split_name_version and if so, set the version to an
unresolved constraint (VersionIntent::Unresolved) instead of using the explicit
version from split_name_version. Keep the existing logic for handling --version
flags in cargo install as exact versions. This distinction between the two
commands ensures that cargo add versions are properly modeled as caret
constraints rather than exact pins.

---

Nitpick comments:
In `@crates/tirith-core/src/scoring.rs`:
- Line 83: The new RuleId::ThreatUnresolvedMaliciousPackage needs to be
explicitly handled in the is_threat_intel_rule_classifies_threat_family function
to ensure proper threat classification behavior and prevent future regressions.
Add a match arm for RuleId::ThreatUnresolvedMaliciousPackage in the
is_threat_intel_rule_classifies_threat_family function that returns true,
alongside any other threat-intel rules that classify threat families.

In `@tests/fixtures/threatintel.toml`:
- Around line 72-105: Add a new fixture to the threatintel.toml file that tests
the non-semver explicit version regression path. Create a fixture with a name
like npm_install_explicit_nonsemantics_malicious_matches that uses input like
"npm install evil-package@latest" or another non-semver token, sets
min_milestone to 1, context to "exec", expected_action to "block", and includes
threat_malicious_package in the expected_rules (not just
threat_unresolved_malicious_package). This ensures that when the threat database
contains the same raw affected version string, the confirmed exact-match
malicious package rule fires correctly for non-semver version specifiers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9d1a759b-3e09-473e-8a37-8f5961fd010b

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2f81f and b06bd84.

📒 Files selected for processing (18)
  • crates/tirith-core/assets/data/rule_explanations.toml
  • crates/tirith-core/build.rs
  • crates/tirith-core/src/ecosystem_scan.rs
  • crates/tirith-core/src/engine.rs
  • crates/tirith-core/src/escalation.rs
  • crates/tirith-core/src/install_txn.rs
  • crates/tirith-core/src/lib.rs
  • crates/tirith-core/src/mcp/output_filter.rs
  • crates/tirith-core/src/rules/threatintel.rs
  • crates/tirith-core/src/scoring.rs
  • crates/tirith-core/src/threatdb.rs
  • crates/tirith-core/src/threatdb_api.rs
  • crates/tirith-core/src/verdict.rs
  • crates/tirith-core/src/version_intent.rs
  • crates/tirith-core/tests/golden_fixtures.rs
  • crates/tirith/src/cli/install.rs
  • tests/fixtures/ecosystem.toml
  • tests/fixtures/threatintel.toml

Comment thread crates/tirith-core/src/ecosystem_scan.rs Outdated
Comment thread crates/tirith-core/src/ecosystem_scan.rs
Comment thread crates/tirith-core/src/rules/threatintel.rs
Comment thread crates/tirith-core/src/threatdb.rs
Comment thread crates/tirith-core/src/version_intent.rs
Comment thread crates/tirith-core/src/version_intent.rs Outdated
sheeki03 added 2 commits June 21, 2026 22:30
…overflow (A1 re-review)

A pathological npm version segment (18446744073709551615) would overflow major+1 /
minor+1 in npm_partial_xrange: a panic in debug, or a bogus >=MAX,<0 range that never
matches in release. checked_add returns None on overflow, so the spec stays an exact
pin (Greptile).
…, don't exclude on empty affected (A1 re-review)

- from_pep440_specifier: a PEP 440 local version (==1.0+ubuntu1) is outside the
  comparable subset and now stays an unresolved Constraint instead of a false Exact
  (CodeRabbit, Critical).
- looks_like_plain_version: reject empty version segments (1., 1..2, 1.+ dynamic
  selector) alongside x/X wildcards, so malformed/dynamic versions are not exact pins
  (CodeRabbit).
- python_requirement_spec: keep the ; environment marker so a marker-qualified
  requirement degrades to an unresolved Constraint, not an unconditional exact pin
  (CodeRabbit).
- assess_package_self: a version-specific malicious record with NO affected versions
  is Unresolved, not a proven ConstraintExcludesAffected exclusion (CodeRabbit).
@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Comment thread crates/tirith-core/src/version_intent.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/tirith-core/src/rules/threatintel.rs (1)

159-181: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Split pip environment markers before marker operators corrupt the package name.

For pip install 'evil; python_version<"3.9"', the current .find('<') sees the marker comparator and makes name_part include evil; python_version, so the threat DB lookup misses evil entirely. Treat ; as a name/spec boundary when it appears before any version operator, while still preserving the marker in rest so it becomes unresolved.

Proposed fix
             let split_pos = pkg_str
                 .find("==")
                 .or_else(|| pkg_str.find(">="))
                 .or_else(|| pkg_str.find("<="))
                 .or_else(|| pkg_str.find("~="))
                 .or_else(|| pkg_str.find("!="))
                 .or_else(|| pkg_str.find('>'))
                 .or_else(|| pkg_str.find('<'));
-            if let Some(pos) = split_pos {
-                (&pkg_str[..pos], &pkg_str[pos..])
-            } else {
-                (pkg_str, "")
+            match (split_pos, pkg_str.find(';')) {
+                (Some(spec_pos), Some(marker_pos)) if marker_pos < spec_pos => {
+                    (&pkg_str[..marker_pos], &pkg_str[marker_pos..])
+                }
+                (None, Some(marker_pos)) => (&pkg_str[..marker_pos], &pkg_str[marker_pos..]),
+                (Some(pos), _) => (&pkg_str[..pos], &pkg_str[pos..]),
+                (None, None) => (pkg_str, ""),
             }
         };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/src/rules/threatintel.rs` around lines 159 - 181, The code
currently searches for version operators (==, >=, <=, etc.) in the entire
pkg_str without first removing environment markers, which causes the find()
chain to incorrectly match operators within marker conditions like
python_version<"3.9". Before applying the version operator search logic, first
split pkg_str at the semicolon (;) to separate the package specification from
any environment markers, then apply the existing find() chain only to the
package spec portion, and finally append the marker portion back to rest to
preserve it for proper handling downstream. Update the split_pos logic and the
name_part/rest tuple construction to work with this marker-aware parsing.
♻️ Duplicate comments (1)
crates/tirith-core/src/ecosystem_scan.rs (1)

661-665: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat Cargo manifest versions as requirements, not exact pins.

Cargo.toml version = "1.0" / { version = "1" } are dependency requirements, but line 664 turns them into Exact via from_explicit_version. Exact only checks the literal/numeric declared token, so a malicious affected version inside the allowed Cargo range can be missed instead of producing the unresolved malicious-package warning. Preserve Cargo manifest specs as unresolved constraints until Cargo semver is modeled. This was previously called out for the Cargo path and still appears here.

Cargo reference specifying dependencies: does `version = "1.2.3"` mean a caret version requirement rather than an exact pin?
Proposed fix
-                        version: version
-                            .map(|v| VersionIntent::from_explicit_version(&v))
-                            .unwrap_or(VersionIntent::Unspecified),
+                        version: version
+                            .map(|v| {
+                                let v = v.trim();
+                                if v.is_empty() {
+                                    VersionIntent::Unspecified
+                                } else {
+                                    VersionIntent::Constraint {
+                                        raw: v.to_string(),
+                                        parsed: None,
+                                    }
+                                }
+                            })
+                            .unwrap_or(VersionIntent::Unspecified),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/src/ecosystem_scan.rs` around lines 661 - 665, The code at
line 664 is treating Cargo manifest versions as exact pins by calling
from_explicit_version, but Cargo manifest version requirements (like "1.0") are
not exact pins and should be preserved as unresolved constraints to avoid
missing malicious versions within allowed ranges. Replace the mapping that calls
from_explicit_version with a direct assignment of VersionIntent::Unresolved to
ensure Cargo manifest dependencies are treated as unresolved requirements rather
than exact versions until Cargo semver is properly modeled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/tirith-core/assets/data/rule_explanations.toml`:
- Around line 1337-1338: Remove the duplicate `[[rule]]` declaration in the
rule_explanations.toml file. The threat_unresolved_malicious_package rule block
has `[[rule]]` appearing twice consecutively, which creates an empty TOML table
entry before the actual rule definition. Keep only one `[[rule]]` declaration
immediately before the `id = "threat_unresolved_malicious_package"` line and
delete the duplicate preceding it.

In `@crates/tirith-core/src/rules/threatintel.rs`:
- Line 303: The version intent parsing at the assignment for version using
intent_from_opt_token does not properly handle npm partial version specifiers
like "4" and "1.2" which npm treats as ranges (4.x, 1.2.x) but are currently
classified as exact pins. Extract or reuse the npm-specific version parsing
logic from ecosystem_scan.rs (specifically the npm_manifest_intent function or
npm_partial_xrange approach) and apply it to the version parameter in this
location to ensure partial versions are correctly interpreted as ranges instead
of exact version matches, aligning the CLI parsing behavior with the manifest
parsing behavior.

In `@crates/tirith-core/src/threatdb.rs`:
- Around line 1061-1088: The code currently allows records with
all_versions_malicious=false and empty rec.versions to fall through to NoRecord
for Exact and Resolved version intents, which misses reporting malicious
packages with incomplete metadata. Add a guard check immediately after the
all_versions_malicious check to handle the case where rec.versions.is_empty() is
true, returning PackageThreatAssessment::Unresolved instead of allowing the
match intent logic to proceed, ensuring that records with missing
affected-version data are treated as unresolved rather than as a non-match.

In `@tests/fixtures/threatintel.toml`:
- Around line 72-105: The test fixture suite is missing regression tests for two
known P1 issues: npm partial-xrange overflow handling (the value
18446744073709551615) and non-semver exact-string matching fallback behavior.
After the existing fixtures that validate unresolved malicious package warnings
(like npm_install_unpinned_malicious_warns_unresolved,
npm_install_range_malicious_warns_unresolved, and
npm_install_unresolved_plus_typosquat_both_fire), add two additional fixture
entries using the same structure shown in the diff. Create one fixture that
tests the npm partial-xrange overflow scenario and another that validates
non-semver exact-string matching fallback behavior, ensuring each has
appropriate input, expected_action, expected_rules, and forbidden_rules to
prevent silent regressions of these known issues.

---

Outside diff comments:
In `@crates/tirith-core/src/rules/threatintel.rs`:
- Around line 159-181: The code currently searches for version operators (==,
>=, <=, etc.) in the entire pkg_str without first removing environment markers,
which causes the find() chain to incorrectly match operators within marker
conditions like python_version<"3.9". Before applying the version operator
search logic, first split pkg_str at the semicolon (;) to separate the package
specification from any environment markers, then apply the existing find() chain
only to the package spec portion, and finally append the marker portion back to
rest to preserve it for proper handling downstream. Update the split_pos logic
and the name_part/rest tuple construction to work with this marker-aware
parsing.

---

Duplicate comments:
In `@crates/tirith-core/src/ecosystem_scan.rs`:
- Around line 661-665: The code at line 664 is treating Cargo manifest versions
as exact pins by calling from_explicit_version, but Cargo manifest version
requirements (like "1.0") are not exact pins and should be preserved as
unresolved constraints to avoid missing malicious versions within allowed
ranges. Replace the mapping that calls from_explicit_version with a direct
assignment of VersionIntent::Unresolved to ensure Cargo manifest dependencies
are treated as unresolved requirements rather than exact versions until Cargo
semver is properly modeled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 482f9f41-136f-4102-8234-d2fdb3d5b560

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2f81f and a44154d.

📒 Files selected for processing (18)
  • crates/tirith-core/assets/data/rule_explanations.toml
  • crates/tirith-core/build.rs
  • crates/tirith-core/src/ecosystem_scan.rs
  • crates/tirith-core/src/engine.rs
  • crates/tirith-core/src/escalation.rs
  • crates/tirith-core/src/install_txn.rs
  • crates/tirith-core/src/lib.rs
  • crates/tirith-core/src/mcp/output_filter.rs
  • crates/tirith-core/src/rules/threatintel.rs
  • crates/tirith-core/src/scoring.rs
  • crates/tirith-core/src/threatdb.rs
  • crates/tirith-core/src/threatdb_api.rs
  • crates/tirith-core/src/verdict.rs
  • crates/tirith-core/src/version_intent.rs
  • crates/tirith-core/tests/golden_fixtures.rs
  • crates/tirith/src/cli/install.rs
  • tests/fixtures/ecosystem.toml
  • tests/fixtures/threatintel.toml

Comment thread crates/tirith-core/assets/data/rule_explanations.toml
Comment thread crates/tirith-core/src/rules/threatintel.rs Outdated
Comment thread crates/tirith-core/src/threatdb.rs
Comment thread tests/fixtures/threatintel.toml
…, unresolved on empty affected, npm-aware CLI specs (A1 re-review r2)

- assess_package_self: a PEP 440 LOCAL version (1.0+ubuntu1) stays Exact and now
  matches both an exact-local DB record (literal) AND a malicious BASE record (1.0),
  so reverting the prior over-broad `+` guard no longer downgrades a real exact-local
  block to Unresolved (Greptile P0) while a malicious base is still caught (CodeRabbit).
- the empty-affected-versions guard moved BEFORE the intent match, so an Exact/Resolved
  request against a versionless malicious record is Unresolved, not a silent NoRecord
  (CodeRabbit).
- npm CLI specs (npm install lodash@4) now use npm_manifest_intent so a bare partial is
  an X-range, matching the manifest path, instead of a bogus Exact("4") that misses a
  record for the resolved 4.17.21 (CodeRabbit).
@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (2)
crates/tirith-core/assets/data/rule_explanations.toml (1)

1337-1338: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the extra [[rule]] table header.

Line 1337 is duplicated consecutively, which creates an unintended empty rule entry before threat_unresolved_malicious_package.

🛠️ Proposed fix
-[[rule]]
 [[rule]]
 id = "threat_unresolved_malicious_package"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/assets/data/rule_explanations.toml` around lines 1337 -
1338, The rule_explanations.toml file has a duplicate [[rule]] table header that
creates an empty rule entry before the threat_unresolved_malicious_package rule
definition. Locate the duplicate [[rule]] header that appears on line 1337 (the
one immediately before id = "threat_unresolved_malicious_package") and remove
it, keeping only one [[rule]] header for this rule entry.
crates/tirith-core/src/ecosystem_scan.rs (1)

661-665: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep Cargo manifest requirements unresolved rather than exact.

Cargo.toml dependency specs are requirements, not lockfile-resolved versions. Passing "1.2.3" through from_explicit_version makes it Exact("1.2.3"), so threat assessment can miss a malicious affected version inside the allowed Cargo range. Since Cargo constraints are not modeled here, preserve the raw spec as an unparsed Constraint. This is the Cargo half of the previously flagged manifest-semver issue.

Cargo dependency version requirement "1.2.3" caret requirement exact pin Cargo reference
Suggested conservative fix
-                        // Cargo semver requirement: not parsed; plain is exact,
-                        // a range stays unresolved.
+                        // Cargo semver requirement: not parsed here; keep it
+                        // unresolved rather than treating manifest specs as
+                        // lockfile-resolved exact versions.
                         version: version
-                            .map(|v| VersionIntent::from_explicit_version(&v))
+                            .map(|v| VersionIntent::Constraint {
+                                raw: v,
+                                parsed: None,
+                            })
                             .unwrap_or(VersionIntent::Unspecified),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/src/ecosystem_scan.rs` around lines 661 - 665, The issue
is that Cargo manifest dependency version specs (like "1.2.3") are being
converted to exact versions via from_explicit_version, but these are
requirements/constraints, not resolved lockfile versions. Replace the call to
from_explicit_version with a mechanism that preserves the raw Cargo spec as an
unparsed constraint instead of converting it to an Exact version intent. When a
version string is present, represent it as an unresolved constraint rather than
an exact match, and only use VersionIntent::Unspecified when no version is
specified.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/tirith-core/src/ecosystem_scan.rs`:
- Around line 497-519: The python_requirement_spec function is incorrectly
identifying comparison operators in environment markers as version specifiers.
For a requirement like django ; python_version < "3.9", the function finds the <
inside the marker and returns it as a version constraint when none exists.
Modify the function to search for version operators only in the substring before
the semicolon (which marks the start of the environment marker), while still
preserving the marker suffix when a real version operator appears before the
semicolon.
- Around line 594-598: The loop over deps.keys() discards the version
specification values entirely, causing all Poetry dependencies to be marked as
VersionIntent::Unspecified regardless of their actual pinned versions. Instead
of only iterating over keys, also extract the corresponding value from deps,
which can be either a string (like "==2.28.1") or an inline table (like {
version = "^2.28" }). Create a helper to parse this value and extract the
version string, then pass the result to VersionIntent::from_pep440_specifier()
in the push() call instead of using VersionIntent::Unspecified. Unsupported
syntax like ^2.0 will naturally degrade to Constraint rather than Unspecified.

In `@crates/tirith-core/src/scoring.rs`:
- Line 83: The new RuleId::ThreatUnresolvedMaliciousPackage rule has been added
to the threat-intel classification set but lacks a corresponding test assertion.
Add a direct positive assertion in the classification test to verify that
RuleId::ThreatUnresolvedMaliciousPackage is correctly mapped to the threat-intel
category. This ensures the classification mapping cannot regress without being
caught by tests.

In `@crates/tirith-core/src/threatdb.rs`:
- Around line 1115-1159: The parsed constraint evaluation block in
VersionIntent::Constraint should only apply PEP 440 semantics for PyPI
ecosystem. Currently, non-PyPI ecosystems can incorrectly return
ConstraintExcludesAffected instead of Unresolved. After successfully unpacking
the parsed constraint (the `Some(constraint)` case), add an ecosystem check to
verify this is PyPI before proceeding with the constraint matching logic using
constraint.matches(rv). If the ecosystem is not PyPI, return
PackageThreatAssessment::Unresolved with UnresolvedReason::ConstraintUnsupported
immediately. Additionally, add a regression test case that verifies a non-PyPI
package record with a parsed constraint (such as npm at 1.0.0 with >=2.0.0)
returns Unresolved, not ConstraintExcludesAffected.

In `@crates/tirith-core/src/version_intent.rs`:
- Around line 11-18: The module documentation starting at line 11 states that
prerelease and local version forms deliberately fail to parse and fall back to
UNRESOLVED, but the implementation and tests show that exact-pin specifiers like
`==1.0.0rc1` and `==1.0+ubuntu1` are kept as Exact constraints instead. Update
the documentation comments in the module preamble (around lines 11-18) and
anywhere else the contract is described (also at lines 97-108 and 374-427) to
clarify that prerelease and local version components are accepted and handled as
Exact constraints when using the equality operator, so the public contract
matches the actual tested behavior.

---

Duplicate comments:
In `@crates/tirith-core/assets/data/rule_explanations.toml`:
- Around line 1337-1338: The rule_explanations.toml file has a duplicate
[[rule]] table header that creates an empty rule entry before the
threat_unresolved_malicious_package rule definition. Locate the duplicate
[[rule]] header that appears on line 1337 (the one immediately before id =
"threat_unresolved_malicious_package") and remove it, keeping only one [[rule]]
header for this rule entry.

In `@crates/tirith-core/src/ecosystem_scan.rs`:
- Around line 661-665: The issue is that Cargo manifest dependency version specs
(like "1.2.3") are being converted to exact versions via from_explicit_version,
but these are requirements/constraints, not resolved lockfile versions. Replace
the call to from_explicit_version with a mechanism that preserves the raw Cargo
spec as an unparsed constraint instead of converting it to an Exact version
intent. When a version string is present, represent it as an unresolved
constraint rather than an exact match, and only use VersionIntent::Unspecified
when no version is specified.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a04a9f64-9be6-4526-be8b-1dea93acdead

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2f81f and 03a4375.

📒 Files selected for processing (18)
  • crates/tirith-core/assets/data/rule_explanations.toml
  • crates/tirith-core/build.rs
  • crates/tirith-core/src/ecosystem_scan.rs
  • crates/tirith-core/src/engine.rs
  • crates/tirith-core/src/escalation.rs
  • crates/tirith-core/src/install_txn.rs
  • crates/tirith-core/src/lib.rs
  • crates/tirith-core/src/mcp/output_filter.rs
  • crates/tirith-core/src/rules/threatintel.rs
  • crates/tirith-core/src/scoring.rs
  • crates/tirith-core/src/threatdb.rs
  • crates/tirith-core/src/threatdb_api.rs
  • crates/tirith-core/src/verdict.rs
  • crates/tirith-core/src/version_intent.rs
  • crates/tirith-core/tests/golden_fixtures.rs
  • crates/tirith/src/cli/install.rs
  • tests/fixtures/ecosystem.toml
  • tests/fixtures/threatintel.toml

Comment thread crates/tirith-core/src/ecosystem_scan.rs
Comment thread crates/tirith-core/src/ecosystem_scan.rs
Comment thread crates/tirith-core/src/scoring.rs
Comment thread crates/tirith-core/src/threatdb.rs
Comment thread crates/tirith-core/src/version_intent.rs Outdated
sheeki03 added 2 commits June 22, 2026 01:33
…ncy version (A1 re-review r2)

For `django ; python_version < "3.9"` (a marker-only requirement with no version spec),
python_requirement_spec found the `<` INSIDE the marker and returned `< "3.9"` as
django's version. Search for the version operator only in the part before the `;`, so a
marker-only dep is Unspecified; a real operator before the marker still keeps the whole
tail (marker included) so from_pep440_specifier rejects it (CodeRabbit).
…t new threat-intel rule classification (A1 re-review r2)

- the module contract doc listed "a local version" among specs that fail to parse to
  UNRESOLVED, but an EXACT local pin (1.0+ubuntu1) is kept as Exact and matched
  literally + by base release; only a local inside a RANGE stays unresolved (CodeRabbit).
- is_threat_intel_rule_classifies_threat_family now asserts
  ThreatUnresolvedMaliciousPackage so the new rule's classification cannot regress
  silently (CodeRabbit).
@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
crates/tirith-core/src/threatdb.rs (1)

1146-1170: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Return proven intersections before falling back to AffectedVersionUnparsed.

This still returns Unresolved on the first unparseable affected version, even when a later parseable affected version satisfies the constraint. Collect matching parseable versions first; only return AffectedVersionUnparsed if no intersection was proven.

🐛 Proposed fix
-                // Every affected version must parse for a proven exclusion;
-                // otherwise we cannot claim the constraint excludes them.
-                let mut parsed_affected: Vec<(String, ReleaseVersion)> =
-                    Vec::with_capacity(rec.versions.len());
+                // A single parsed affected version that matches proves intersection.
+                // Every affected version must parse only for a proven exclusion.
+                let mut intersecting = Vec::new();
+                let mut saw_unparsed_affected = false;
                 for v in &rec.versions {
                     match ReleaseVersion::parse(v) {
-                        Some(rv) => parsed_affected.push((v.to_string(), rv)),
+                        Some(rv) if constraint.matches(&rv) => intersecting.push(v.to_string()),
+                        Some(_) => {}
                         None => {
-                            return PackageThreatAssessment::Unresolved {
-                                summary,
-                                reason: UnresolvedReason::AffectedVersionUnparsed,
-                                affected_versions: affected,
-                            };
+                            saw_unparsed_affected = true;
                         }
                     }
                 }
-                let intersecting: Vec<String> = parsed_affected
-                    .iter()
-                    .filter(|(_, rv)| constraint.matches(rv))
-                    .map(|(s, _)| s.clone())
-                    .collect();
-                if intersecting.is_empty() {
-                    PackageThreatAssessment::ConstraintExcludesAffected
-                } else {
+                if !intersecting.is_empty() {
                     PackageThreatAssessment::ConstraintIntersectsAffected {
                         summary,
                         affected_versions: intersecting,
                     }
+                } else if saw_unparsed_affected {
+                    PackageThreatAssessment::Unresolved {
+                        summary,
+                        reason: UnresolvedReason::AffectedVersionUnparsed,
+                        affected_versions: affected,
+                    }
+                } else {
+                    PackageThreatAssessment::ConstraintExcludesAffected
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/src/threatdb.rs` around lines 1146 - 1170, The code
currently returns PackageThreatAssessment::Unresolved with
AffectedVersionUnparsed as soon as it encounters a single version that cannot be
parsed, rather than continuing to check other versions. Refactor the loop to
continue processing all versions regardless of parse failures, collecting both
the parseable versions and tracking whether any parse failures occurred. After
the loop completes, only return the AffectedVersionUnparsed error if the
intersecting vector is empty AND there were versions that failed to parse during
iteration. This ensures that proven intersections with parseable affected
versions are returned before falling back to the AffectedVersionUnparsed
unresolved state.
🧹 Nitpick comments (1)
crates/tirith-core/src/threatdb.rs (1)

1553-1559: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Align the merge doc with the provenance-preserving behavior.

The doc still says affected versions are deduplicated from every layer, but the implementation intentionally returns affected versions only from the layer whose summary is reported. Please update this comment so future changes do not reintroduce cross-source misattribution.

♻️ Proposed doc fix
-/// either layer is unresolved the result is `Unresolved` with deduplicated
-/// affected versions drawn from every layer that names any (so the warning
-/// lists them all); else a concrete intersection (deduped); else a proven
-/// exclusion; else `NoRecord`. Crucially, `ConstraintExcludesAffected` (which
+/// either layer is unresolved the result is `Unresolved` with the summary,
+/// reason, and affected versions from the reported layer; else a concrete
+/// intersection with affected versions from the reported layer; else a proven
+/// exclusion; else `NoRecord`. Crucially, `ConstraintExcludesAffected` (which
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/src/threatdb.rs` around lines 1553 - 1559, The
documentation comment describing the merge precedence behavior incorrectly
states that affected versions are deduplicated from every layer that names any,
but the actual implementation returns affected versions only from the layer
whose summary is reported. Update the Precedence documentation comment to
accurately reflect that affected versions are sourced only from the layer whose
summary is being returned, not from all layers, to ensure the documentation
aligns with the actual provenance-preserving behavior and prevents future
regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@crates/tirith-core/src/threatdb.rs`:
- Around line 1146-1170: The code currently returns
PackageThreatAssessment::Unresolved with AffectedVersionUnparsed as soon as it
encounters a single version that cannot be parsed, rather than continuing to
check other versions. Refactor the loop to continue processing all versions
regardless of parse failures, collecting both the parseable versions and
tracking whether any parse failures occurred. After the loop completes, only
return the AffectedVersionUnparsed error if the intersecting vector is empty AND
there were versions that failed to parse during iteration. This ensures that
proven intersections with parseable affected versions are returned before
falling back to the AffectedVersionUnparsed unresolved state.

---

Nitpick comments:
In `@crates/tirith-core/src/threatdb.rs`:
- Around line 1553-1559: The documentation comment describing the merge
precedence behavior incorrectly states that affected versions are deduplicated
from every layer that names any, but the actual implementation returns affected
versions only from the layer whose summary is reported. Update the Precedence
documentation comment to accurately reflect that affected versions are sourced
only from the layer whose summary is being returned, not from all layers, to
ensure the documentation aligns with the actual provenance-preserving behavior
and prevents future regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d06fad52-6bfa-4260-bb30-da3d9e039093

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2f81f and 3270b6b.

📒 Files selected for processing (18)
  • crates/tirith-core/assets/data/rule_explanations.toml
  • crates/tirith-core/build.rs
  • crates/tirith-core/src/ecosystem_scan.rs
  • crates/tirith-core/src/engine.rs
  • crates/tirith-core/src/escalation.rs
  • crates/tirith-core/src/install_txn.rs
  • crates/tirith-core/src/lib.rs
  • crates/tirith-core/src/mcp/output_filter.rs
  • crates/tirith-core/src/rules/threatintel.rs
  • crates/tirith-core/src/scoring.rs
  • crates/tirith-core/src/threatdb.rs
  • crates/tirith-core/src/threatdb_api.rs
  • crates/tirith-core/src/verdict.rs
  • crates/tirith-core/src/version_intent.rs
  • crates/tirith-core/tests/golden_fixtures.rs
  • crates/tirith/src/cli/install.rs
  • tests/fixtures/ecosystem.toml
  • tests/fixtures/threatintel.toml

@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

Comment thread crates/tirith-core/src/ecosystem_scan.rs
…sion spec (A1 re-review)

python_requirement_spec received the raw line, so `malware==1.0.0 # pinned safe version` carried
the comment into the version token. from_pep440_specifier then saw `==1.0.0 # ...`, failed
looks_like_plain_version (the space), and produced Constraint{parsed:None} -> Unresolved. Net
effect: an EXACT pin of the affected version was silently downgraded from ThreatMaliciousPackage
(Critical/Block) to ThreatUnresolvedMaliciousPackage (Medium/Warn), and a safe pin got a spurious
warn. Drop the pip-style ` #` / `\t#` inline comment before the operator search (Greptile P1).
@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
crates/tirith-core/assets/data/rule_explanations.toml (1)

1337-1338: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove the duplicated [[rule]] header at Line 1337.

There are two consecutive [[rule]] entries before the new rule body. This introduces an empty table element and can break rule explanation parsing.

Proposed fix
-[[rule]]
 [[rule]]
 id = "threat_unresolved_malicious_package"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tirith-core/assets/data/rule_explanations.toml` around lines 1337 -
1338, Remove the duplicate [[rule]] header that appears consecutively before the
threat_unresolved_malicious_package rule definition. Keep only one [[rule]]
header before the id = "threat_unresolved_malicious_package" line. The duplicate
entry creates an empty table element that breaks the rule explanation parsing in
the TOML file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@crates/tirith-core/assets/data/rule_explanations.toml`:
- Around line 1337-1338: Remove the duplicate [[rule]] header that appears
consecutively before the threat_unresolved_malicious_package rule definition.
Keep only one [[rule]] header before the id =
"threat_unresolved_malicious_package" line. The duplicate entry creates an empty
table element that breaks the rule explanation parsing in the TOML file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 47966446-48da-437d-8c2c-f03e8ca4ca2f

📥 Commits

Reviewing files that changed from the base of the PR and between 9b2f81f and 9d68a19.

📒 Files selected for processing (18)
  • crates/tirith-core/assets/data/rule_explanations.toml
  • crates/tirith-core/build.rs
  • crates/tirith-core/src/ecosystem_scan.rs
  • crates/tirith-core/src/engine.rs
  • crates/tirith-core/src/escalation.rs
  • crates/tirith-core/src/install_txn.rs
  • crates/tirith-core/src/lib.rs
  • crates/tirith-core/src/mcp/output_filter.rs
  • crates/tirith-core/src/rules/threatintel.rs
  • crates/tirith-core/src/scoring.rs
  • crates/tirith-core/src/threatdb.rs
  • crates/tirith-core/src/threatdb_api.rs
  • crates/tirith-core/src/verdict.rs
  • crates/tirith-core/src/version_intent.rs
  • crates/tirith-core/tests/golden_fixtures.rs
  • crates/tirith/src/cli/install.rs
  • tests/fixtures/ecosystem.toml
  • tests/fixtures/threatintel.toml

if last.ecosystem == Ecosystem::RubyGems
&& matches!(last.version, VersionIntent::Unspecified)
{
last.version = VersionIntent::from_explicit_version(ver);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 from_explicit_version does not strip the leading = that Ruby Gems CLI passes as part of a version constraint, so gem install evil-pkg --version "= 1.0.0" produces Constraint { raw: "= 1.0.0" }Unresolved (Medium/Warn) instead of Exact("1.0.0")ExactMatch (Critical/Block). from_gem_version already handles this case by stripping the leading = before delegating to from_explicit_version, and is the correct helper to use here — it is used consistently in gemfile_gem_spec for the same ecosystem.

Suggested change
last.version = VersionIntent::from_explicit_version(ver);
last.version = VersionIntent::from_gem_version(ver);

Fix in Claude Code

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@sheeki03

Copy link
Copy Markdown
Owner Author

@greptileai review

@sheeki03

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

…es so an attacker-cased dist excludes itself from threat-DB lookup (A1 re-review)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant