diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 979269f..b384915 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,346 +4,24 @@ on: push: branches: [main] pull_request: - branches: [main] -env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: "0" +permissions: + contents: read jobs: - test: - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - # Pin to the same toolchain as rust-toolchain.toml (timeglyph is an app: - # declared MSRV == the pinned toolchain). Do not float `toolchain: stable`. - - uses: dtolnay/rust-toolchain@1.96.0 - with: - components: clippy, rustfmt - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - cache-on-failure: true - # --workspace / --all are load-bearing: with a root package present, cargo's - # default selection is that package ALONE, so a bare `cargo test` would - # silently skip `core/` (timeglyph-core) and its tests would never gate. - - run: cargo test --workspace --all-features - - run: cargo clippy --workspace --all-targets --all-features -- -D warnings - - run: cargo fmt --all --check - - # The lens (GUI overlay) is a separate crate, excluded from the root workspace, - # so the `test` job never touches it. Build it natively on each shipped - # platform to compile + LINK the platform picker CI otherwise never sees — UI - # Automation on Windows (x86_64, the exact shipped target), the Accessibility - # API on macOS, and AT-SPI on Linux. - lens: - name: Lens build (${{ matrix.os }}) - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - name: Install GUI build deps (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get update - # mesa-vulkan-drivers supplies the lavapipe software Vulkan ICD. The lens's - # offscreen render gate asks wgpu for an adapter, and a runner with no ICD has - # none — the gate would panic rather than check anything. The all-black / - # tofu frame regression it catches (see Cargo.toml on default_fonts) is - # platform-independent and worth running here, so give it an adapter. - sudo apt-get install -y libxkbcommon-dev libgl1-mesa-dev libwayland-dev \ - libx11-dev libxcursor-dev libxrandr-dev libxi-dev libxcb1-dev pkg-config \ - mesa-vulkan-drivers - - uses: dtolnay/rust-toolchain@1.96.0 - with: - components: clippy, rustfmt - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - cache-on-failure: true - - name: Build lens (compiles + links the platform picker) - working-directory: lens - run: cargo build --all-targets - # Gate the lens's own tests, not just that it compiles. Without this the - # clipboard privacy/caption tests and the offscreen render gate are local-only, - # so a regression in them reaches main green. - - name: Test lens - working-directory: lens - run: cargo test - - name: Clippy lens - working-directory: lens - run: cargo clippy --all-targets -- -D warnings - - name: Rustfmt lens - working-directory: lens - run: cargo fmt --check - - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: dtolnay/rust-toolchain@1.96.0 - with: - components: llvm-tools-preview - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - cache-on-failure: true - - uses: taiki-e/install-action@a402910a723481c4c80d006d75298c796a9c8695 # cargo-llvm-cov - with: - tool: cargo-llvm-cov - # All test targets, all features (integration tests carry the coverage, so - # NOT --lib). The gate requires 100% LINE **and** function coverage of the - # LIBRARY (ADR-0008), honoring `// cov:unreachable` markers on - # provably-dead defensive arms (raw --fail-under-lines can't exempt - # those); main.rs (the Humble CLI shell) is excluded by the script. - # The JSON carries per-function counts, the LCOV per-line `DA:` hits — the - # gate needs both, and exits 2 if either is missing. - # - # BOTH commands must name every workspace member. `report` does not inherit - # the package selection of the run before it and rejects `--workspace`, so - # with a bare `report --lcov` the LCOV omits `core/` entirely — the LINE - # half of the gate then passes over timeglyph-core having measured nothing, - # while the JSON/function half still sees it. That asymmetry is what makes - # the hole quiet, so a new member must be added to the `-p` list here. - - name: Coverage + 100% line/function gate - run: | - cargo llvm-cov --workspace --all-features --json --output-path cov.json - cargo llvm-cov report -p timeglyph -p timeglyph-core --lcov --output-path lcov.info - python3 scripts/coverage-gate.py cov.json lcov.info - - name: Upload to Codecov - uses: codecov/codecov-action@0f8570b1a125f4937846a11fcfa3bcd548bd8c97 # v4.6.0 - with: - files: lcov.info - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false - - msrv: - # timeglyph-core declares `rust-version = "1.75"` — a low floor downstreams - # (FOUNDATION crates, published low-MSRV parsers) pin against. An MSRV nobody - # enforces is an unbacked promise, so this job is what makes the number true. - # Raising the floor is near-breaking; do it deliberately, in both places the - # assert below names. - # - # It builds core/ COPIED OUT of the repo, and that is load-bearing rather - # than incidental. Two things in the tree would otherwise decide the outcome - # instead of the crate: the workspace Cargo.lock is version 4, which cargo - # 1.75 refuses to parse at all (it fails before ever reading core/), and - # rust-toolchain.toml pins 1.96.0, which would override the toolchain this - # job just installed. Outside the repo the crate is its own workspace with - # its own lockfile — exactly how a crates.io consumer builds it. Do NOT - # "simplify" this by downgrading or deleting the committed v4 lockfile. - name: MSRV 1.75 (timeglyph-core) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: dtolnay/rust-toolchain@1.75.0 - - name: Assert the declared MSRV is the one this job checks - run: | - declared=$(grep -m1 '^rust-version' core/Cargo.toml | cut -d'"' -f2) - if [ "$declared" != "1.75" ]; then - echo "core/Cargo.toml declares rust-version = \"$declared\", but this job installs 1.75.0." - echo "The two must move together, or the floor stops being verified. Update both." - exit 1 - fi - - name: Check + test timeglyph-core on its declared MSRV, standalone - run: | - dest="$RUNNER_TEMP/timeglyph-core-msrv" - cp -R core "$dest" - cd "$dest" - # `cargo +1.75.0`, not a bare `cargo`: the explicit +toolchain sits at - # the top of rustup's precedence chain, above RUSTUP_TOOLCHAIN, a - # directory override and any rust-toolchain.toml. A bare `cargo` here - # is silently hostage to ambient state — verified locally, where an - # exported RUSTUP_TOOLCHAIN=1.96.0 beat `rustup override set 1.75.0` - # and compiled the crate on 1.96 while the job looked green. - # - # Then assert the compiler rather than merely logging it. If the - # toolchain ever fails to resolve as intended, a newer cargo would - # build this crate happily and the job would pass having verified - # nothing — the vacuous-gate shape, same as a coverage report that - # contains no records for the file it claims to cover. - version=$(cargo +1.75.0 --version) - echo "$version" - case "$version" in - "cargo 1.75.0"*) ;; - *) echo "Expected cargo 1.75.0, got: $version — the MSRV was not actually exercised."; exit 1 ;; - esac - cargo +1.75.0 check --all-targets - cargo +1.75.0 test - - name: Assert zero dependencies - # The zero-dependency promise is what lets a downstream link this crate - # for free; enforce it rather than trust the manifest comment. The - # lockfile cargo just resolved must name exactly one package — itself. - run: | - dest="$RUNNER_TEMP/timeglyph-core-msrv" - count=$(grep -c '^\[\[package\]\]' "$dest/Cargo.lock") - if [ "$count" != "1" ]; then - echo "timeglyph-core resolved $count packages; it must resolve exactly 1 (itself):" - grep '^name = ' "$dest/Cargo.lock" - exit 1 - fi - - secrets: - name: Secret Scan (gitleaks) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 - # The gitleaks GitHub Action requires a paid license for org repos; the - # gitleaks CLI is free. Install + run it directly (fleet pattern). - - name: Install gitleaks - # Pin the version rather than query the GitHub API for "latest": the - # unauthenticated api.github.com call rate-limits on shared CI IPs, which - # left VERSION empty and 404'd the download. Bump deliberately. - shell: bash - run: | - set -euo pipefail - # renovate: datasource=github-releases depName=gitleaks/gitleaks - VERSION=8.30.1 - # Retried: a one-off network blip on a shared runner should not fail a - # job that has nothing to do with the network. Bounded so it cannot - # become a hang either. - fetch() { - local url="$1" out="$2" attempt=1 - until curl -sSfL --max-time 120 "$url" -o "$out"; do - if [ "$attempt" -ge 3 ]; then - echo "::error::download failed on all 3 attempts: $url (endpoint unreachable, not a code failure)" - return 1 - fi - echo "::warning::download failed (attempt ${attempt}/3), retrying: $url" - rm -f "$out" - sleep $((attempt * 15)) - attempt=$((attempt + 1)) - done - } - fetch "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/gitleaks_${VERSION}_linux_x64.tar.gz" /tmp/gitleaks.tar.gz - tar xz -C /tmp -f /tmp/gitleaks.tar.gz gitleaks - /tmp/gitleaks version - - name: Run gitleaks - run: /tmp/gitleaks detect --source . --config .gitleaks.toml - - deny: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - - name: Install cargo-deny - uses: taiki-e/install-action@a402910a723481c4c80d006d75298c796a9c8695 - with: - tool: cargo-deny - - name: Check advisories, licenses, bans, sources - run: cargo deny check - - freshness: - # Advisory dependency-freshness gate: fails if the committed Cargo.lock is - # stale relative to the requirements (see CLAUDE.md "Dependency Freshness"). - runs-on: ubuntu-latest - continue-on-error: true - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable - # BOTH locks. `lens/` is excluded from the workspace and carries its own - # Cargo.lock, which this job never looked at -- so it drifted while the - # check reported on the root alone and read as covering the repo. A gate - # that inspects one of two artifacts is not a gate on the repo. - - run: cargo update --locked - - run: cargo update --locked --manifest-path lens/Cargo.toml - - lean-build: - # Gate: `cargo add timeglyph --no-default-features` must NOT pull in clap, - # csv, or tracing-subscriber. Those are CLI-only deps; a library consumer - # doing `default-features = false` should get a lean build with only the - # core engine deps (jiff, thiserror, hex, serde, serde_json, tracing facade). - # Enforced now that clap/csv/tracing-subscriber sit behind the `cli`/`csv` - # features (a lean library build carries none of them). - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: dtolnay/rust-toolchain@1.96.0 - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - cache-on-failure: true - - name: Lean lib build (no default features) - run: cargo build --no-default-features - - name: Assert clap / csv / tracing-subscriber absent from lean tree - run: | - cargo tree --no-default-features -e no-dev --prefix none > /tmp/lean-tree.txt - UNWANTED=$(grep -E '^clap |^csv |^tracing-subscriber ' /tmp/lean-tree.txt || true) - if [ -n "$UNWANTED" ]; then - echo "FAIL: CLI-only deps present in lean (no-default-features) build:" - echo "$UNWANTED" - exit 1 - fi - - wasm-build: - # Gate: the core engine must compile to wasm32 so the WASM playground and - # browser / CyberChef embeddings stay buildable. Depends on the engine being - # pure (no I/O, no clap/csv) and jiff + serde being wasm-compatible. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: dtolnay/rust-toolchain@1.96.0 - with: - targets: wasm32-unknown-unknown - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - cache-on-failure: true - - name: WASM lib build (wasm32-unknown-unknown, no default features) - run: cargo build --lib --no-default-features --target wasm32-unknown-unknown - - name: WASM playground binding build (wasm32) - run: cargo build --manifest-path wasm/Cargo.toml --target wasm32-unknown-unknown - - python-wheel: - # Gate: the PyO3 Python bindings build a wheel and its tests pass. Publishing - # to PyPI is a separate credentialed release step; this proves the wheel is - # always buildable and correct (plaso/Timesketch/Jupyter consume it). - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: dtolnay/rust-toolchain@1.96.0 - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - with: - cache-on-failure: true - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: "3.11" - - run: pip install maturin pytest - # One v* tag ships the crate AND the wheel, so the wheel version must equal - # the crate version. release-plz bumps ONLY the root crate on a release, so an - # equality *check* here can never pass on a release PR (bindings/ would still - # lag). Instead stamp the bindings from the crate — the single source of truth — - # making drift impossible by construction (this closes the 0.3.0-vs-0.4.0 gap - # without a guard release-plz can't satisfy). The same stamp runs in the wheel - # build (release.yml) so published wheels are always at the crate version. - - name: sync bindings version to the crate - run: ./bindings/python/sync-version.sh - - name: Build + test the Python wheel - working-directory: bindings/python - run: | - maturin build --release - pip install target/wheels/*.whl - pytest tests/ -q - - vet: - name: Cargo Vet (supply-chain) - runs-on: ubuntu-latest - # Complements `deny` (known-bad advisories/licenses) with the supply-chain- - # injection layer: every dependency version must be human-source-reviewed or - # covered by an imported aggregate audit set (Google/Mozilla/Bytecode-Alliance/ - # Embark). Config in supply-chain/{config,audits,imports}.toml. - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: dtolnay/rust-toolchain@stable - - name: Install cargo-vet - uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6 - with: - tool: cargo-vet - - name: Fetch dependencies - run: cargo fetch - - name: Check supply chain - run: cargo vet --locked + ci: + uses: SecurityRonin/fleet-ci/.github/workflows/rust-ci.yml@1721d98b44b10fec8902969f80fa8b4ee5c06960 + with: + # STRICT on lines AND functions -- what the bespoke gate this replaces + # enforced, and stronger than the line-only gate 47 consumers run. It is + # only expressible now: the shared gate previously demanded a marker on + # every line of a multi-line dead arm, and counted duplicate crate + # compilations as uncovered functions. Both are fixed upstream. + coverage-gate: strict + coverage-metric: both + coverage-ignore-regex: '(^|/)src/(main\.rs|bin/)' + # The workspace floor. `timeglyph-core`'s published 1.75 promise CANNOT be + # verified in-tree -- this repo's Cargo.lock is v4 and cargo 1.75 refuses + # to parse it -- so that check stays in extras.yml, where it copies the + # crate out first, and is required by branch protection alongside this gate. + msrv: "1.96.0" diff --git a/.github/workflows/extras.yml b/.github/workflows/extras.yml new file mode 100644 index 0000000..747c2e1 --- /dev/null +++ b/.github/workflows/extras.yml @@ -0,0 +1,231 @@ +# The checks that the shared fleet workflow cannot express, carved out of +# ci.yml when the commodity jobs moved to it. +# +# These are NOT part of `ci / All checks`. A job outside the reusable workflow +# publishes its own context, so branch protection reaches it only if it is named +# there explicitly. `MSRV 1.75 (timeglyph-core)` is required alongside the +# aggregate gate for exactly that reason -- it verifies a published floor that +# downstreams pin against, and a promise nothing enforces is not a promise. +# +# Why each cannot move: +# +# MSRV 1.75 (timeglyph-core) the shared msrv job builds in-tree, and this +# repo's Cargo.lock is v4, which cargo 1.75 cannot +# parse. Verifying the floor means copying the +# crate out first -- and asserting the resolved +# lock names exactly one package, which is what +# makes "zero dependencies" a checked claim rather +# than a README sentence. +# Lens build `lens/` is EXCLUDED from the workspace, so no +# `--workspace` invocation reaches it. +# lean-build asserts clap/csv/tracing-subscriber are ABSENT +# from the `--no-default-features` tree. The shared +# workflow builds; it does not assert absence. +# wasm-build a wasm32 target build plus an out-of-workspace +# binding crate. +# python-wheel maturin + pytest against the built wheel. +# freshness `cargo update --locked` on both lockfiles. +name: extras + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + lens: + name: Lens build (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Install GUI build deps (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + # mesa-vulkan-drivers supplies the lavapipe software Vulkan ICD. The lens's + # offscreen render gate asks wgpu for an adapter, and a runner with no ICD has + # none — the gate would panic rather than check anything. The all-black / + # tofu frame regression it catches (see Cargo.toml on default_fonts) is + # platform-independent and worth running here, so give it an adapter. + sudo apt-get install -y libxkbcommon-dev libgl1-mesa-dev libwayland-dev \ + libx11-dev libxcursor-dev libxrandr-dev libxi-dev libxcb1-dev pkg-config \ + mesa-vulkan-drivers + - uses: dtolnay/rust-toolchain@1.96.0 + with: + components: clippy, rustfmt + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + cache-on-failure: true + - name: Build lens (compiles + links the platform picker) + working-directory: lens + run: cargo build --all-targets + # Gate the lens's own tests, not just that it compiles. Without this the + # clipboard privacy/caption tests and the offscreen render gate are local-only, + # so a regression in them reaches main green. + - name: Test lens + working-directory: lens + run: cargo test + - name: Clippy lens + working-directory: lens + run: cargo clippy --all-targets -- -D warnings + - name: Rustfmt lens + working-directory: lens + run: cargo fmt --check + msrv: + # timeglyph-core declares `rust-version = "1.75"` — a low floor downstreams + # (FOUNDATION crates, published low-MSRV parsers) pin against. An MSRV nobody + # enforces is an unbacked promise, so this job is what makes the number true. + # Raising the floor is near-breaking; do it deliberately, in both places the + # assert below names. + # + # It builds core/ COPIED OUT of the repo, and that is load-bearing rather + # than incidental. Two things in the tree would otherwise decide the outcome + # instead of the crate: the workspace Cargo.lock is version 4, which cargo + # 1.75 refuses to parse at all (it fails before ever reading core/), and + # rust-toolchain.toml pins 1.96.0, which would override the toolchain this + # job just installed. Outside the repo the crate is its own workspace with + # its own lockfile — exactly how a crates.io consumer builds it. Do NOT + # "simplify" this by downgrading or deleting the committed v4 lockfile. + name: MSRV 1.75 (timeglyph-core) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: dtolnay/rust-toolchain@1.75.0 + - name: Assert the declared MSRV is the one this job checks + run: | + declared=$(grep -m1 '^rust-version' core/Cargo.toml | cut -d'"' -f2) + if [ "$declared" != "1.75" ]; then + echo "core/Cargo.toml declares rust-version = \"$declared\", but this job installs 1.75.0." + echo "The two must move together, or the floor stops being verified. Update both." + exit 1 + fi + - name: Check + test timeglyph-core on its declared MSRV, standalone + run: | + dest="$RUNNER_TEMP/timeglyph-core-msrv" + cp -R core "$dest" + cd "$dest" + # `cargo +1.75.0`, not a bare `cargo`: the explicit +toolchain sits at + # the top of rustup's precedence chain, above RUSTUP_TOOLCHAIN, a + # directory override and any rust-toolchain.toml. A bare `cargo` here + # is silently hostage to ambient state — verified locally, where an + # exported RUSTUP_TOOLCHAIN=1.96.0 beat `rustup override set 1.75.0` + # and compiled the crate on 1.96 while the job looked green. + # + # Then assert the compiler rather than merely logging it. If the + # toolchain ever fails to resolve as intended, a newer cargo would + # build this crate happily and the job would pass having verified + # nothing — the vacuous-gate shape, same as a coverage report that + # contains no records for the file it claims to cover. + version=$(cargo +1.75.0 --version) + echo "$version" + case "$version" in + "cargo 1.75.0"*) ;; + *) echo "Expected cargo 1.75.0, got: $version — the MSRV was not actually exercised."; exit 1 ;; + esac + cargo +1.75.0 check --all-targets + cargo +1.75.0 test + - name: Assert zero dependencies + # The zero-dependency promise is what lets a downstream link this crate + # for free; enforce it rather than trust the manifest comment. The + # lockfile cargo just resolved must name exactly one package — itself. + run: | + dest="$RUNNER_TEMP/timeglyph-core-msrv" + count=$(grep -c '^\[\[package\]\]' "$dest/Cargo.lock") + if [ "$count" != "1" ]; then + echo "timeglyph-core resolved $count packages; it must resolve exactly 1 (itself):" + grep '^name = ' "$dest/Cargo.lock" + exit 1 + fi + freshness: + # Advisory dependency-freshness gate: fails if the committed Cargo.lock is + # stale relative to the requirements (see CLAUDE.md "Dependency Freshness"). + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable + # BOTH locks. `lens/` is excluded from the workspace and carries its own + # Cargo.lock, which this job never looked at -- so it drifted while the + # check reported on the root alone and read as covering the repo. A gate + # that inspects one of two artifacts is not a gate on the repo. + - run: cargo update --locked + - run: cargo update --locked --manifest-path lens/Cargo.toml + lean-build: + # Gate: `cargo add timeglyph --no-default-features` must NOT pull in clap, + # csv, or tracing-subscriber. Those are CLI-only deps; a library consumer + # doing `default-features = false` should get a lean build with only the + # core engine deps (jiff, thiserror, hex, serde, serde_json, tracing facade). + # Enforced now that clap/csv/tracing-subscriber sit behind the `cli`/`csv` + # features (a lean library build carries none of them). + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: dtolnay/rust-toolchain@1.96.0 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + cache-on-failure: true + - name: Lean lib build (no default features) + run: cargo build --no-default-features + - name: Assert clap / csv / tracing-subscriber absent from lean tree + run: | + cargo tree --no-default-features -e no-dev --prefix none > /tmp/lean-tree.txt + UNWANTED=$(grep -E '^clap |^csv |^tracing-subscriber ' /tmp/lean-tree.txt || true) + if [ -n "$UNWANTED" ]; then + echo "FAIL: CLI-only deps present in lean (no-default-features) build:" + echo "$UNWANTED" + exit 1 + fi + wasm-build: + # Gate: the core engine must compile to wasm32 so the WASM playground and + # browser / CyberChef embeddings stay buildable. Depends on the engine being + # pure (no I/O, no clap/csv) and jiff + serde being wasm-compatible. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: dtolnay/rust-toolchain@1.96.0 + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + cache-on-failure: true + - name: WASM lib build (wasm32-unknown-unknown, no default features) + run: cargo build --lib --no-default-features --target wasm32-unknown-unknown + - name: WASM playground binding build (wasm32) + run: cargo build --manifest-path wasm/Cargo.toml --target wasm32-unknown-unknown + python-wheel: + # Gate: the PyO3 Python bindings build a wheel and its tests pass. Publishing + # to PyPI is a separate credentialed release step; this proves the wheel is + # always buildable and correct (plaso/Timesketch/Jupyter consume it). + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: dtolnay/rust-toolchain@1.96.0 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + cache-on-failure: true + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.11" + - run: pip install maturin pytest + # One v* tag ships the crate AND the wheel, so the wheel version must equal + # the crate version. release-plz bumps ONLY the root crate on a release, so an + # equality *check* here can never pass on a release PR (bindings/ would still + # lag). Instead stamp the bindings from the crate — the single source of truth — + # making drift impossible by construction (this closes the 0.3.0-vs-0.4.0 gap + # without a guard release-plz can't satisfy). The same stamp runs in the wheel + # build (release.yml) so published wheels are always at the crate version. + - name: sync bindings version to the crate + run: ./bindings/python/sync-version.sh + - name: Build + test the Python wheel + working-directory: bindings/python + run: | + maturin build --release + pip install target/wheels/*.whl + pytest tests/ -q diff --git a/core/Cargo.toml b/core/Cargo.toml index 25e2613..cb458f7 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" # fleet's library floor and is CI-verified. Raising it is near-breaking. rust-version = "1.75" license = "Apache-2.0" +readme = "README.md" description = "Zero-dependency epoch arithmetic: Windows FILETIME, WebKit/Chrome, Cocoa and HFS+ timestamps to Unix nanoseconds, with the not-set sentinel policy." authors = ["Albert Hui "] repository = "https://github.com/SecurityRonin/timeglyph" diff --git a/core/LICENSE b/core/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/core/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..7301a69 --- /dev/null +++ b/core/README.md @@ -0,0 +1,29 @@ +# timeglyph-core + +The integer half of [timeglyph](https://github.com/SecurityRonin/timeglyph): +turning platform epoch values into Unix nanoseconds. + +**Zero dependencies**, and the dependency count is a checked claim rather than a +README sentence — CI asserts the resolved lockfile names exactly one package. + +```toml +[dependencies] +timeglyph-core = "0.1" +``` + +Handles Windows `FILETIME`, WebKit/Chrome, Cocoa and HFS+ timestamps, along with +the not-set sentinel policy that distinguishes "the epoch" from "never set" — +a distinction that matters when the value came out of an artifact rather than a +clock. + +This crate is deliberately narrow: it does arithmetic and nothing else. The +scanning, scoring and rendering that decide *which* interpretation of some bytes +is plausible live in `timeglyph`, and are a different kind of claim. + +Its MSRV is **1.75**, verified in CI by building the crate standalone. + +--- + +[Privacy](https://securityronin.github.io/timeglyph/privacy/) · +[Terms](https://securityronin.github.io/timeglyph/terms/) · +© Security Ronin Ltd diff --git a/core/src/lib.rs b/core/src/lib.rs index 3c1ae65..3e3d78e 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -24,7 +24,7 @@ //! makes negative values ordinary — every date before 2001 is one — so only //! `0.0` and the non-finite values are rejected. //! -//! This is deliberately **not** the contract of [`timeglyph`]'s scanner, which +//! This is deliberately **not** the contract of `timeglyph`'s scanner, which //! answers a different question — "what *could* this value decode to?" — and so //! reports the 1601 instant for a zero FILETIME rather than hiding it. Neither //! is a fallback for the other; pick by what your caller does with the answer. diff --git a/lens/Cargo.lock b/lens/Cargo.lock index 72b7511..4b60953 100644 --- a/lens/Cargo.lock +++ b/lens/Cargo.lock @@ -1302,9 +1302,9 @@ dependencies = [ [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "emath" diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 0f87b44..8f8aeea 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,8 @@ [toolchain] channel = "1.96.0" +# Required, not optional. `rust-toolchain.toml` OVERRIDES a workflow that asks +# for the `stable` channel -- a channel ref loses to the toml -- so cargo runs +# this pinned toolchain while the workflow installed rustfmt and clippy for +# `stable`. The result is `'cargo-fmt' is not installed for the toolchain +# '1.96.0'`, which reads as a broken workflow and is a missing declaration here. +components = ["clippy", "rustfmt"] diff --git a/src/cal.rs b/src/cal.rs index 33390fc..076c787 100644 --- a/src/cal.rs +++ b/src/cal.rs @@ -1,5 +1,5 @@ //! `timeglyph cal` — a timezone/DST/leap-aware reference calendar. This module is the pure data -//! builder: [`build_day`] computes the civil + timezone facts of a date (ISO +//! builder: [`cal::build_day`](crate::cal::build_day) computes the civil + timezone facts of a date (ISO //! week, day-of-year, Julian Day Number, Modified JD, Unix midnight, weekday, //! per-day UTC offset, DST fold/gap, and — behind the `leap` feature — leap-second //! days and GPS week) with zero I/O, so it is fully testable and serialisable. diff --git a/src/cal_color.rs b/src/cal_color.rs index cc1d84e..6baa6fa 100644 --- a/src/cal_color.rs +++ b/src/cal_color.rs @@ -28,7 +28,7 @@ pub struct Ink { } impl ColorMode { - /// Wrap `text` in the SGR for `ink` at this capability (no-op under [`Mono`]). + /// Wrap `text` in the SGR for `ink` at this capability (no-op under [`ColorMode::Mono`]). #[must_use] pub fn paint(self, ink: Ink, text: &str) -> String { let (r, g, b) = ink.rgb; @@ -40,7 +40,7 @@ impl ColorMode { } } - /// Wrap `text` in reverse video (used for `today`); no-op under [`Mono`]. + /// Wrap `text` in reverse video (used for `today`); no-op under [`ColorMode::Mono`]. #[must_use] pub fn reverse(self, text: &str) -> String { if self == ColorMode::Mono { diff --git a/src/cal_render.rs b/src/cal_render.rs index 8e1bbc1..2c0e465 100644 --- a/src/cal_render.rs +++ b/src/cal_render.rs @@ -1,4 +1,4 @@ -//! Pure renderers for [`crate::cal`] — turn a [`CalMonth`] into a monospace text +//! Pure renderers for [`crate::cal`] — turn a [`CalMonth`](crate::cal::CalMonth) into a monospace text //! calendar for the terminal. No I/O, no colour side effects here: the grid uses //! spaces only (never box-drawing characters, which misalign across fonts), one //! marker glyph per day, and an ISO-week gutter. Machine (`--json`) output is the diff --git a/src/carve.rs b/src/carve.rs index 700acee..c166116 100644 --- a/src/carve.rs +++ b/src/carve.rs @@ -1,4 +1,4 @@ -//! Bounded carve: slide [`identify_bytes`](crate::interpret::identify_bytes) over +//! Bounded carve: slide [`crate::interpret::identify_bytes`] over //! a blob and report scored hits per offset — the validatable core of "find //! timestamps in raw bytes" (a malware config blob, a hex selection, one record). //! @@ -27,7 +27,7 @@ const MAX_CARVE_BYTES: usize = 4096; /// Slide [`identify_bytes`] over every offset of `bytes` and return each reading /// whose score ≥ `min_score` and whose instant falls in `window` (nanoseconds, /// `[lo, hi)`, if given), tagged with its offset. Sorted by offset, then score -/// descending. At most [`MAX_CARVE_BYTES`] of input is swept. +/// descending. At most `MAX_CARVE_BYTES` of input is swept. #[must_use] pub fn carve(bytes: &[u8], min_score: f64, window: Option<(i128, i128)>) -> Vec { let n = bytes.len().min(MAX_CARVE_BYTES); diff --git a/src/holiday.rs b/src/holiday.rs index 2cf22a5..2fc5197 100644 --- a/src/holiday.rs +++ b/src/holiday.rs @@ -12,7 +12,7 @@ //! the covered data", not "provably an ordinary day". //! //! Cost: the ~1.5 MB blob is inflated and parsed once, lazily, on the first -//! lookup (an [`OnceLock`]); the parsed table is then held for the process +//! lookup (a [`OnceLock`](std::sync::OnceLock)); the parsed table is then held for the process //! lifetime (tens of MB). Both are opt-in with the feature. use std::collections::HashMap; @@ -43,6 +43,10 @@ fn table() -> &'static Table { /// runtime failure means a packaging regression — logged at error level, never a /// silent empty table that reads like "no holidays". fn load_or_empty(what: &str, decoded: Result) -> T { + // cov:unreachable: the embedded blobs are decoded and validated in CI, so + // this arm cannot run for a build that shipped. Kept — and logged at error + // level rather than returning a silent empty table — so a packaging + // regression announces itself instead of reading as "no holidays". decoded.unwrap_or_else(|e| { tracing::error!(table = what, error = %e, "embedded holiday data failed to decode; degrading to empty"); T::default() diff --git a/src/interpret.rs b/src/interpret.rs index 0ab1679..bc27f4f 100644 --- a/src/interpret.rs +++ b/src/interpret.rs @@ -1660,7 +1660,7 @@ fn parse_asn1_utctime(s: &str) -> Option<(PosixNs, bool)> { /// A per-format spec card generated from the registry — epoch, tick unit, /// timezone/leap semantics, plausible range, known value sentinels, and the -/// citation. Registry-derived (a projection of the same [`Format`](crate::Format) +/// citation. Registry-derived (a projection of the same [`crate::Format`] /// the engine decodes with), so it never drifts from the decoder. `None` for an /// unknown format id. #[must_use] diff --git a/src/lunisolar.rs b/src/lunisolar.rs index 846c421..ea1eeec 100644 --- a/src/lunisolar.rs +++ b/src/lunisolar.rs @@ -5,7 +5,7 @@ //! conversion is **convention-relative**: a UTC instant maps to a lunisolar date //! only once a *reference meridian* is fixed (China uses UTC+8; Vietnam UTC+7; //! Korea UTC+9), because the calendar assigns civil DAYS at that meridian. So -//! [`render`] REQUIRES a [`RenderZone`]. The optional `longitude` corrects the +//! [`render`](crate::lunisolar::render) REQUIRES a [`RenderZone`]. The optional `longitude` corrects the //! HOUR pillar to local mean solar time (真太陽時); the equation of time is NOT //! applied (stated in the reading's assumptions). //! diff --git a/src/mcp.rs b/src/mcp.rs index ca8a9d4..032775a 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -2,7 +2,7 @@ //! as tools — `identify`, `decode`, `explain` — so an LLM-driven DFIR workflow //! gets a cited, reproducible reading instead of a hallucinated epoch conversion //! (LLMs are reliably wrong at FILETIME↔Unix arithmetic). The handler is a pure -//! function; the `mcp` subcommand is a thin stdin→[`handle`]→stdout loop over it. +//! function; the `mcp` subcommand is a thin stdin→[`handle`](crate::mcp::handle)→stdout loop over it. use serde_json::{json, Value}; diff --git a/src/registry.rs b/src/registry.rs index 554afc5..b3cff82 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -7,7 +7,7 @@ //! the 16 packed bit-field layouts — FAT/DOS, exFAT, DTTM, BitDate/BitDec, BCD/GSM //! semi-octet, Motorola, Symantec, DVR, NS40/NS40LE, LogTime, Nokia LE, SQL Server //! — and builds [`FORMATS`] by wrapping each catalog entry in an engine [`Format`] -//! with the matching [`crate::PackedCodec`]. The SYSTEMTIME packed and +//! with the matching `PackedCodec`. The SYSTEMTIME packed and //! ULID/UUIDv1/RFC-2822/EXIF string forms live in `interpret.rs`; the leap-aware //! GPS/NTP/TAI scales in `leap.rs`. Each packed codec is cross-validated against //! the MIT `time-decode` oracle (tests/oracle.rs, tests/catalog.rs), and diff --git a/src/scan.rs b/src/scan.rs index 3430895..3a74410 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -1,7 +1,7 @@ //! Scan arbitrary text for timestamp candidates and decode each into ranked -//! readings. Three extractors — long digit runs ([`scan_numbers`]), -//! self-describing datetime strings ([`datetime_candidates`]), and raw-hex tokens -//! ([`hex_candidates`]) — feed [`interpret`](crate::interpret); [`inspect_text`] +//! readings. Three extractors — long digit runs ([`scan_numbers`](crate::scan::scan_numbers)), +//! self-describing datetime strings (`datetime_candidates`), and raw-hex tokens +//! (`hex_candidates`) — feed [`crate::interpret`]; [`inspect_text`](crate::scan::inspect_text) //! ties them together. Pure and GUI-free: it powers both the CLI `scan` command //! and the timeglyph-lens overlay. @@ -310,7 +310,7 @@ fn string_readings_opts( /// Candidate datetime-string substrings of `text`: the whole text, each line, /// and each whitespace token (deduped, >= 8 chars). Pure numeric runs are -/// excluded — those are the integer path's job ([`scan_numbers`]). +/// excluded — those are the integer path's job ([`scan_numbers`](crate::scan::scan_numbers)). fn datetime_candidates(text: &str) -> Vec { let mut seen = std::collections::BTreeSet::new(); let mut out = Vec::new();