From e3617eef4a6d2821efbc424690ded64698e7cfdc Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Fri, 13 Nov 2026 17:27:54 +0800 Subject: [PATCH] Add min dep age 7 days --- .cargo/config.toml | 6 + .mise/config.toml | 6 +- CHECKS.md | 12 +- config/conftest/policy/cross/publish_age.rego | 108 ++++++++++++++++ .../policy/cross/publish_age_test.rego | 122 ++++++++++++++++++ 5 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 config/conftest/policy/cross/publish_age.rego create mode 100644 config/conftest/policy/cross/publish_age_test.rego diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..aabd9e8d --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,6 @@ +# Hold back registry crate versions younger than a week. +# A malicious or broken release is usually yanked within that window, before the resolver can pick it. Versions +# already in Cargo.lock are kept. For an urgent bump, such as a security fix, run +# `CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow cargo update -p --precise `. +[registry] +global-min-publish-age = "7 days" diff --git a/.mise/config.toml b/.mise/config.toml index ade24e57..7a1f9bb7 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -55,6 +55,9 @@ java.shorthand_vendor = "openjdk" # `mise lock --bump` re-resolves the `latest` selectors when a deliberate tool refresh is wanted. lockfile = true lockfile_platforms = ["linux-arm64", "linux-x64", "macos-arm64", "macos-x64", "windows-x64"] +# Hide tool releases younger than a week, so a malicious or broken release is usually pulled before it resolves. +# mise's own default is a day. Cargo holds crates back by the same age. +minimum_release_age = "7d" task.run_auto_install = false # Hard-verify tool signatures via gpg on every platform. # Propagates to node.gpg_verify and swift.gpg_verify per mise/src/config/settings.rs. mise's node plugin @@ -1398,7 +1401,8 @@ description = "Run conftest OPA/Rego policies over the TOML config + lock files" # combined TOML input. run = """ ns_list="cross cargo mise mise_lock pyproject checksums advisories" -named="Cargo.lock config/cargo-features.toml config/upstream-cache/data.toml config/deny.toml config/pyrefly.toml" +named=".cargo/config.toml Cargo.lock config/cargo-features.toml config/upstream-cache/data.toml config/deny.toml" +named="$named config/pyrefly.toml" # Both lists rely on word-splitting when they expand. # $ns expands to repeated `--namespace ` pairs; $named is a space-separated path list. # shellcheck disable=SC2086 diff --git a/CHECKS.md b/CHECKS.md index d6525bef..bfeb797c 100644 --- a/CHECKS.md +++ b/CHECKS.md @@ -5,7 +5,7 @@ - **Tools:** 49 - **Shell-script checks:** 11 - **Checks with a repair pass:** 22 -- **Rules:** 388 custom local rules, or non-default strict linter settings +- **Rules:** 397 custom local rules, or non-default strict linter settings - **Meta rules:** 22 of those rules are rules about rules Every check is listed across every `MISE_ENV`, filed under the tool it runs. Each entry names the env whose config @@ -672,6 +672,16 @@ Rules, from `config/conftest/policy`: - Every recorded asset must name its `upstream` project URL. - Every recorded asset must carry its `license` as an SPDX expression. - Every recorded asset must carry a `sha256`, left empty only while bootstrapping. +- `publish_age` (`config/conftest/policy/cross/publish_age.rego`): + - Cargo's publish age lives in .cargo/config.toml, so the file must exist. + - .cargo/config.toml must set cargo's publish age. + - .mise/config.toml must set mise's release age. + - Cargo's publish age must be a number and a unit word, so it can be compared with mise's. + - mise's release age must be a number and a unit letter, so it can be compared with cargo's. + - Neither age may be zero, which turns the cooldown off. + - Cargo and mise must hold dependencies back by the same age. + - A registry-specific cargo age replaces the global one for that registry, so it must also match mise's. + - The resolver override that lets too-new versions through is for one-off runs, never committed. - `pyrefly_python_sync` (`config/conftest/policy/cross/pyrefly_python_sync.rego`): - pyrefly must pin the Python version it type-checks against. - pyrefly's pinned Python version must equal the mise `python` pin, the version every lane installs. diff --git a/config/conftest/policy/cross/publish_age.rego b/config/conftest/policy/cross/publish_age.rego new file mode 100644 index 00000000..24b538cb --- /dev/null +++ b/config/conftest/policy/cross/publish_age.rego @@ -0,0 +1,108 @@ +# The cargo and mise dependency cooldowns must name the same age, and neither may be switched off. +# mise writes the age as `7d` and cargo as `7 days`, so the two match when their numbers are equal and their units +# start with the same letter. Cargo's `months` is not accepted: its `m` would pair with mise's minutes. +package cross + +import data.paths + +mise_age_pattern := `^([0-9]+)([smhdw])$` + +cargo_age_pattern := `^([0-9]+) (second|minute|hour|day|week)s?$` + +cargo_config := file.contents if { + some file in paths.files + file.path == ".cargo/config.toml" +} + +mise_config := file.contents if { + some file in paths.files + file.path == ".mise/config.toml" +} + +cargo_age := cargo_config.registry["global-min-publish-age"] + +mise_age := mise_config.settings.minimum_release_age + +# An age as [number, first letter of its unit], undefined when the value is not in the tool's accepted form. +age_parts(value, pattern) := [to_number(match[1]), substring(match[2], 0, 1)] if { + is_string(value) + some match in regex.find_all_string_submatch_n(pattern, value, 1) +} + +cargo_parts := age_parts(cargo_age, cargo_age_pattern) + +mise_parts := age_parts(mise_age, mise_age_pattern) + +# Every cargo age that outranks the global one, as [where it is set, value]. +cargo_override contains ["[registry] min-publish-age", age] if { + age := cargo_config.registry["min-publish-age"] +} + +cargo_override contains [where, age] if { + some name, registry in cargo_config.registries + age := registry["min-publish-age"] + where := $"[registries.{name}] min-publish-age" +} + +matches_mise(age) if age_parts(age, cargo_age_pattern) == mise_parts + +zero_age if { + some parts in [cargo_parts, mise_parts] + parts[0] == 0 +} + +# Cargo's publish age lives in .cargo/config.toml, so the file must exist. +deny contains "publish age: .cargo/config.toml is missing; it sets cargo's [registry] global-min-publish-age" if { + not cargo_config +} + +# .cargo/config.toml must set cargo's publish age. +deny contains "publish age: .cargo/config.toml sets no [registry] global-min-publish-age" if { + cargo_config + not cargo_age +} + +# .mise/config.toml must set mise's release age. +deny contains "publish age: .mise/config.toml sets no [settings] minimum_release_age" if { + mise_config + not mise_age +} + +# Cargo's publish age must be a number and a unit word, so it can be compared with mise's. +deny contains msg if { + cargo_age + not cargo_parts + msg := $"publish age: cargo global-min-publish-age \"{cargo_age}\" is not ` `" +} + +# mise's release age must be a number and a unit letter, so it can be compared with cargo's. +deny contains msg if { + mise_age + not mise_parts + msg := $"publish age: mise minimum_release_age \"{mise_age}\" is not ``" +} + +# Neither age may be zero, which turns the cooldown off. +deny contains msg if { + zero_age + msg := $"publish age: cargo \"{cargo_age}\" / mise \"{mise_age}\" must not be zero, which turns the cooldown off" +} + +# Cargo and mise must hold dependencies back by the same age. +deny contains msg if { + cargo_parts != mise_parts + msg := $"publish age: cargo global-min-publish-age \"{cargo_age}\" != mise minimum_release_age \"{mise_age}\"" +} + +# A registry-specific cargo age replaces the global one for that registry, so it must also match mise's. +deny contains msg if { + some override in cargo_override + [where, age] := override + not matches_mise(age) + msg := $"publish age: cargo {where} \"{age}\" != mise minimum_release_age \"{mise_age}\"" +} + +# The resolver override that lets too-new versions through is for one-off runs, never committed. +deny contains "publish age: .cargo/config.toml must not set [resolver] incompatible-publish-age = \"allow\"" if { + cargo_config.resolver["incompatible-publish-age"] == "allow" +} diff --git a/config/conftest/policy/cross/publish_age_test.rego b/config/conftest/policy/cross/publish_age_test.rego new file mode 100644 index 00000000..082a61b9 --- /dev/null +++ b/config/conftest/policy/cross/publish_age_test.rego @@ -0,0 +1,122 @@ +# Unit tests for the cargo/mise publish-age cross-check, run by `conftest verify`. +# +# The real task only ever sees two ages that already agree, which looks the same as a rule that never matches. +package cross_test + +import data.cross + +age_cargo_file(cargo) := {"path": ".cargo/config.toml", "contents": cargo} + +age_mise_file(mise) := {"path": ".mise/config.toml", "contents": mise} + +age_input(cargo_age, mise_age) := [ + age_cargo_file({"registry": {"global-min-publish-age": cargo_age}}), + age_mise_file({"settings": {"minimum_release_age": mise_age}}), +] + +age_reports(msgs, fragment) if { + some msg in msgs + startswith(msg, "publish age:") + contains(msg, fragment) +} + +age_clean(msgs) if { + not any_age_report(msgs) +} + +any_age_report(msgs) if { + some msg in msgs + startswith(msg, "publish age:") +} + +test_age_days_matching_is_accepted if { + age_clean(cross.deny) with input as age_input("7 days", "7d") +} + +test_age_singular_week_matching_is_accepted if { + age_clean(cross.deny) with input as age_input("1 week", "1w") +} + +test_age_windows_paths_are_still_read if { + files := age_input("7 days", "7d") + cargo := object.union(files[0], {"path": `.cargo\config.toml`}) + windows := [cargo, object.union(files[1], {"path": `.mise\config.toml`})] + age_clean(cross.deny) with input as windows +} + +test_age_missing_cargo_config_is_flagged if { + msgs := cross.deny with input as [age_mise_file({"settings": {"minimum_release_age": "7d"}})] + age_reports(msgs, ".cargo/config.toml is missing") +} + +test_age_missing_cargo_key_is_flagged if { + files := [age_cargo_file({"registry": {}}), age_mise_file({"settings": {"minimum_release_age": "7d"}})] + msgs := cross.deny with input as files + age_reports(msgs, "sets no [registry] global-min-publish-age") +} + +test_age_missing_mise_key_is_flagged if { + files := [age_cargo_file({"registry": {"global-min-publish-age": "7 days"}}), age_mise_file({"settings": {}})] + msgs := cross.deny with input as files + age_reports(msgs, "sets no [settings] minimum_release_age") +} + +test_age_bare_zero_is_flagged if { + msgs := cross.deny with input as age_input("0", "0") + age_reports(msgs, "global-min-publish-age \"0\" is not") + age_reports(msgs, "minimum_release_age \"0\" is not") +} + +test_age_zero_with_unit_is_flagged if { + msgs := cross.deny with input as age_input("0 days", "0d") + age_reports(msgs, "must not be zero") +} + +test_age_months_is_flagged if { + msgs := cross.deny with input as age_input("7 months", "7m") + age_reports(msgs, "global-min-publish-age \"7 months\" is not") +} + +test_age_unparsable_mise_value_is_flagged if { + msgs := cross.deny with input as age_input("7 days", "2026-01-01") + age_reports(msgs, "minimum_release_age \"2026-01-01\" is not") +} + +test_age_different_numbers_are_flagged if { + msgs := cross.deny with input as age_input("5 days", "7d") + age_reports(msgs, "\"5 days\" != mise minimum_release_age \"7d\"") +} + +test_age_different_units_are_flagged if { + msgs := cross.deny with input as age_input("7 weeks", "7d") + age_reports(msgs, "\"7 weeks\" != mise minimum_release_age \"7d\"") +} + +age_with_registry(registry, registries) := [ + age_cargo_file({"registry": registry, "registries": registries}), + age_mise_file({"settings": {"minimum_release_age": "7d"}}), +] + +test_age_matching_registry_override_is_accepted if { + files := age_with_registry({"global-min-publish-age": "7 days"}, {"crates-io": {"min-publish-age": "7 days"}}) + age_clean(cross.deny) with input as files +} + +test_age_zero_registry_override_is_flagged if { + files := age_with_registry({"global-min-publish-age": "7 days"}, {"crates-io": {"min-publish-age": "0"}}) + msgs := cross.deny with input as files + age_reports(msgs, "[registries.crates-io] min-publish-age \"0\" != mise") +} + +test_age_shorter_crates_io_age_is_flagged if { + registry := {"global-min-publish-age": "7 days", "min-publish-age": "5 days"} + msgs := cross.deny with input as age_with_registry(registry, {}) + age_reports(msgs, "[registry] min-publish-age \"5 days\" != mise") +} + +test_age_committed_allow_is_flagged if { + cargo := {"registry": {"global-min-publish-age": "7 days"}, "resolver": {"incompatible-publish-age": "allow"}} + files := [age_cargo_file(cargo), age_mise_file({"settings": {"minimum_release_age": "7d"}})] + msgs := cross.deny with input as files + age_reports(msgs, "incompatible-publish-age") +}