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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .github/workflows/dependency-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Dependency audit

on:
push:
branches: [main]
paths:
- "package.json"
- "pnpm-lock.yaml"
- "Cargo.toml"
- "Cargo.lock"
- "apps/desktop/src-tauri/Cargo.toml"
- "engines/drawscope-engine/pyproject.toml"
- "engines/drawscope-engine/uv.lock"
- ".github/workflows/dependency-audit.yml"
pull_request:
paths:
- "package.json"
- "pnpm-lock.yaml"
- "Cargo.toml"
- "Cargo.lock"
- "apps/desktop/src-tauri/Cargo.toml"
- "engines/drawscope-engine/pyproject.toml"
- "engines/drawscope-engine/uv.lock"
- ".github/workflows/dependency-audit.yml"
schedule:
- cron: "43 8 3 * *"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: dependency-audit-${{ github.ref }}
cancel-in-progress: true

jobs:
javascript:
name: JavaScript lockfile
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24

- name: Set up pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
with:
version: 9.15.0
run_install: false

- name: Audit the complete JavaScript lockfile
run: pnpm audit --audit-level high

python:
name: Python lockfile
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"

- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version: "0.11.30"
enable-cache: true

- name: Export the frozen Python dependency graph
run: >-
uv export --project engines/drawscope-engine --frozen --all-groups
--no-emit-project --format requirements.txt
--output-file "${RUNNER_TEMP}/drawscope-audit-requirements.txt" --quiet

- name: Audit the exported Python lock graph
run: >-
uvx --from pip-audit==2.10.1 pip-audit
--requirement "${RUNNER_TEMP}/drawscope-audit-requirements.txt"
--strict --progress-spinner off

rust:
name: Rust lockfile
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Install cargo-audit
run: cargo install cargo-audit --locked --version 0.22.2

- name: Audit the complete Rust lockfile
run: cargo audit --file Cargo.lock
2 changes: 2 additions & 0 deletions DEPENDENCY_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ Dependabot groups compatible minor and patch updates by ecosystem. Every group m
Automated version-update pull requests intentionally exclude semantic-major upgrades. Major upgrades are reviewed deliberately against migration notes, data compatibility, release behavior, and rollback plans. Security updates remain enabled and are evaluated independently of this cadence.

GitHub Actions are pinned to immutable commit identities with their release tag noted in comments. Dependabot may propose monthly action updates; maintainers verify the upstream repository and release notes before merge.

The dedicated dependency-audit workflow covers the complete pnpm lockfile, the frozen uv runtime-and-development graph, and the complete Cargo lockfile. Known vulnerabilities fail their language-specific job. RustSec informational warnings are reviewed in full and may remain only when target reachability and the lack of a compatible fix are documented; they are never hidden behind an unrecorded ignore. See [`docs/DEPENDENCY-AUDIT.md`](docs/DEPENDENCY-AUDIT.md) for the reproducible commands and dated evidence.
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
<p align="center">
<a href="https://github.com/NouraldinFarge/drawscope/actions/workflows/ci.yml"><img alt="CI status" src="https://github.com/NouraldinFarge/drawscope/actions/workflows/ci.yml/badge.svg"></a>
<a href="https://github.com/NouraldinFarge/drawscope/actions/workflows/codeql.yml"><img alt="CodeQL status" src="https://github.com/NouraldinFarge/drawscope/actions/workflows/codeql.yml/badge.svg"></a>
<a href="https://github.com/NouraldinFarge/drawscope/actions/workflows/dependency-audit.yml"><img alt="Dependency audit status" src="https://github.com/NouraldinFarge/drawscope/actions/workflows/dependency-audit.yml/badge.svg"></a>
<a href="https://github.com/NouraldinFarge/drawscope/releases/latest"><img alt="Latest release" src="https://img.shields.io/github/v/release/NouraldinFarge/drawscope?display_name=tag&sort=semver"></a>
<a href="#verified-archive-snapshot"><img alt="Archive snapshot 2026-07-28" src="https://img.shields.io/badge/archive-2026--07--28-7a68d8"></a>
<a href="#verified-archive-snapshot"><img alt="Dated archive snapshot 2026-07-28" src="https://img.shields.io/badge/archive-2026--07--28-7a68d8"></a>
<img alt="Windows x64" src="https://img.shields.io/badge/platform-Windows%20x64-0078D4">
<img alt="Local first" src="https://img.shields.io/badge/data-local--first-18a67b">
<a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-4f8cff"></a>
Expand Down Expand Up @@ -104,14 +105,16 @@ The Rust layer owns persistence, validation, migrations, file boundaries, and si

Snapshot date: **2026-07-28** · Latest captured draw: **2026-07-28** · Known gaps: **4**

| Game | Coverage | Draws | Sessions |
| --- | ---: | ---: | ---: |
| Powerball | 1992-04-22 → 2026-07-27 | 3,813 | 1 |
| Mega Millions | 2002-05-17 → 2026-07-24 | 2,522 | 1 |
| Illinois Lotto | 2014-01-20 → 2026-07-27 | 1,960 | 1 |
| Lucky Day Lotto | 2014-01-19 → 2026-07-28 | 9,147 | 2 |
| Pick 3 | 2010-01-01 → 2026-07-28 | 12,078 | 2 |
| Pick 4 | 2010-01-01 → 2026-07-28 | 12,078 | 2 |
This is a dated offline evidence snapshot—not live lottery data. The [weekly freshness workflow](.github/workflows/archive-freshness.yml) flags a refresh as due after 14 days and stale after 30; it never invents missing rows or substitutes an unreviewed source.

**Coverage by game**

- **Powerball:** 1992-04-22 → 2026-07-27 · 3,813 draws · 1 session
- **Mega Millions:** 2002-05-17 → 2026-07-24 · 2,522 draws · 1 session
- **Illinois Lotto:** 2014-01-20 → 2026-07-27 · 1,960 draws · 1 session
- **Lucky Day Lotto:** 2014-01-19 → 2026-07-28 · 9,147 draws · 2 sessions
- **Pick 3:** 2010-01-01 → 2026-07-28 · 12,078 draws · 2 sessions
- **Pick 4:** 2010-01-01 → 2026-07-28 · 12,078 draws · 2 sessions

Two isolated frozen-source rebuilds produced the same 41,394,176-byte SQLite database:

Expand Down Expand Up @@ -174,7 +177,7 @@ pnpm dev
- **Understand the product:** [documentation hub](docs/README.md), [responsible use](docs/RESPONSIBLE-USE.md), [known limitations](docs/KNOWN-LIMITATIONS.md)
- **Review the research:** [worked case study](docs/CASE-STUDY.md), [methodology](docs/METHODOLOGY.md), [packaged-run evidence](examples/powerball-retrospective-v0.6.5/README.md), [source research](docs/SOURCE-RESEARCH.md), [database reconstruction](docs/DATABASE.md)
- **Review the engineering:** [architecture](docs/ARCHITECTURE.md), [contracts](docs/CONTRACTS.md), [testing](docs/TESTING.md), [function inventory](docs/FUNCTION-INVENTORY.md)
- **Operate or assess risk:** [runbooks](docs/RUNBOOKS.md), [distribution/signing](docs/DISTRIBUTION.md), [maintenance policy](docs/MAINTENANCE.md), [security model](docs/SECURITY.md), [dependency policy](DEPENDENCY_POLICY.md), [accessibility](docs/ACCESSIBILITY.md)
- **Operate or assess risk:** [runbooks](docs/RUNBOOKS.md), [distribution/signing](docs/DISTRIBUTION.md), [maintenance policy](docs/MAINTENANCE.md), [security model](docs/SECURITY.md), [dependency-audit evidence](docs/DEPENDENCY-AUDIT.md), [dependency policy](DEPENDENCY_POLICY.md), [accessibility](docs/ACCESSIBILITY.md)

## Contributing and support

Expand Down
27 changes: 27 additions & 0 deletions docs/DEPENDENCY-AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Dependency-audit evidence

DrawScope keeps three independently locked dependency graphs because its shipped desktop path crosses JavaScript/TypeScript, Rust, and Python. The dedicated [`Dependency audit`](../.github/workflows/dependency-audit.yml) workflow checks all three graphs on relevant pull requests and pushes, monthly, and on demand. It has read-only repository permissions and uses immutable GitHub Action revisions.

## Reproduce the checks

From the repository root:

```powershell
pnpm audit --audit-level high

$auditRequirements = Join-Path $env:TEMP "drawscope-audit-requirements.txt"
uv export --project engines/drawscope-engine --frozen --all-groups `
--no-emit-project --format requirements.txt --output-file $auditRequirements --quiet
uvx --from pip-audit==2.10.1 pip-audit `
--requirement $auditRequirements --strict --progress-spinner off

cargo audit --file Cargo.lock
```

The JavaScript check includes development tooling because build and test dependencies can still affect CI and release integrity. The Python check exports the complete frozen runtime-and-development graph from `uv.lock`; it excludes only the local `drawscope-engine` package itself. The Rust check rejects known vulnerabilities while reporting informational maintenance and soundness advisories for review.

## Current reviewed result

On 2026-08-25, all three commands reported **no known vulnerabilities** in their locked graphs. `cargo audit 0.22.2` also reported 17 informational warnings inherited through Tauri: twelve affect GTK3 or its macro chain and are absent from the compiled Windows target, while five mark legacy Unicode crates in Tauri's `urlpattern` chain as unmaintained. The Unicode crates are reachable during the Windows build, but RustSec does not identify an exploitable vulnerability or a compatible patched line. The warnings remain visible in audit output and in [known limitations](KNOWN-LIMITATIONS.md); they are not suppressed or described as fixed.

This dated result is evidence for one lockfile state, not a permanent security claim. Dependabot, CodeQL, monthly audit runs, release review, and the private vulnerability-reporting channel remain separate controls.
5 changes: 5 additions & 0 deletions docs/MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ viewport, and reduced-motion preference.

## Dependency maintenance

The dedicated [`Dependency audit`](../.github/workflows/dependency-audit.yml) workflow
checks the complete pnpm, uv, and Cargo lock graphs on relevant changes, monthly, and on
demand. Its exact commands, scope, and current dated result are recorded in
[`DEPENDENCY-AUDIT.md`](DEPENDENCY-AUDIT.md).

Dependabot updates are reviewed as normal code changes. Combine superseded patch-level
updates when the lockfile can be regenerated and verified once; do not merge competing
lockfiles independently. Follow [`DEPENDENCY_POLICY.md`](../DEPENDENCY_POLICY.md) for
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ flowchart TB
- [`RUNBOOKS.md`](RUNBOOKS.md) — build, release, recovery, and incident procedures
- [`DISTRIBUTION.md`](DISTRIBUTION.md) — portable/installer packaging, signing, and publication gates
- [`MAINTENANCE.md`](MAINTENANCE.md) — freshness, dependency, presentation, and release cadence
- [`DEPENDENCY-AUDIT.md`](DEPENDENCY-AUDIT.md) — reproducible JavaScript, Python, and Rust lock-graph checks
- [`STANDARDS-COMPLIANCE.md`](STANDARDS-COMPLIANCE.md) — applicable standards and compliance posture

### Historical audit trail
Expand Down
32 changes: 32 additions & 0 deletions scripts/archive-presentation.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export function formatInteger(value) {
return new Intl.NumberFormat("en-US").format(value);
}

export function renderArchiveCoverage(data) {
return data.games
.map(
(game) =>
`- **${game.name}:** ${game.firstDraw} → ${game.lastDraw} · ${formatInteger(game.drawCount)} draws · ${game.sessions} ${game.sessions === 1 ? "session" : "sessions"}`,
)
.join("\n");
}

export function renderArchiveSummary(data) {
return `<!-- drawscope:archive-summary:start -->
## Verified archive snapshot

Snapshot date: **${data.snapshotDate}** · Latest captured draw: **${data.latestDraw}** · Known gaps: **${data.knownGapCount}**

This is a dated offline evidence snapshot—not live lottery data. The [weekly freshness workflow](.github/workflows/archive-freshness.yml) flags a refresh as due after 14 days and stale after 30; it never invents missing rows or substitutes an unreviewed source.

**Coverage by game**

${renderArchiveCoverage(data)}

Two isolated frozen-source rebuilds produced the same ${data.databaseBytesLabel}-byte SQLite database:

\`\`\`text
SHA-256 ${data.databaseSha256}
\`\`\`
<!-- drawscope:archive-summary:end -->`;
}
1 change: 1 addition & 0 deletions scripts/build-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ await Promise.all([
writeFile(path.join(outputRoot, "index.html"), html, "utf8"),
copyFile(path.join(siteRoot, "styles.css"), path.join(outputRoot, "styles.css")),
copyFile(path.join(siteRoot, "app.js"), path.join(outputRoot, "app.js")),
copyFile(path.join(siteRoot, "freshness.mjs"), path.join(outputRoot, "freshness.mjs")),
copyFile(path.join(siteRoot, "favicon.svg"), path.join(outputRoot, "favicon.svg")),
writeFile(path.join(outputRoot, ".nojekyll"), "", "utf8"),
writeFile(
Expand Down
1 change: 1 addition & 0 deletions scripts/check-documentation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ async function checkProjectSite() {
for (const relative of [
"styles.css",
"app.js",
"freshness.mjs",
"favicon.svg",
".nojekyll",
"robots.txt",
Expand Down
34 changes: 4 additions & 30 deletions scripts/presentation-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import path from "node:path";

import { formatInteger } from "./archive-presentation.mjs";

export { renderArchiveCoverage, renderArchiveSummary } from "./archive-presentation.mjs";

export const root = fileURLToPath(new URL("..", import.meta.url));

const gameOrder = ["powerball", "mega-millions", "lotto", "lucky-day-lotto", "pick-3", "pick-4"];
Expand All @@ -15,10 +19,6 @@ const presentationNames = {
"pick-4": "Pick 4",
};

function formatInteger(value) {
return new Intl.NumberFormat("en-US").format(value);
}

export async function loadPresentationData() {
const [version, manifestSource, catalogSource, sourceCatalogSource] = await Promise.all([
readFile(path.join(root, "VERSION"), "utf8"),
Expand Down Expand Up @@ -70,32 +70,6 @@ export async function loadPresentationData() {
};
}

export function renderArchiveTable(data) {
const rows = data.games
.map(
(game) =>
`| ${game.name} | ${game.firstDraw} → ${game.lastDraw} | ${formatInteger(game.drawCount)} | ${game.sessions} |`,
)
.join("\n");
return `| Game | Coverage | Draws | Sessions |\n| --- | ---: | ---: | ---: |\n${rows}`;
}

export function renderArchiveSummary(data) {
return `<!-- drawscope:archive-summary:start -->
## Verified archive snapshot

Snapshot date: **${data.snapshotDate}** · Latest captured draw: **${data.latestDraw}** · Known gaps: **${data.knownGapCount}**

${renderArchiveTable(data)}

Two isolated frozen-source rebuilds produced the same ${data.databaseBytesLabel}-byte SQLite database:

\`\`\`text
SHA-256 ${data.databaseSha256}
\`\`\`
<!-- drawscope:archive-summary:end -->`;
}

export function replaceMarkedBlock(source, name, replacement) {
const start = `<!-- drawscope:${name}:start -->`;
const end = `<!-- drawscope:${name}:end -->`;
Expand Down
46 changes: 46 additions & 0 deletions scripts/presentation-regressions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { readFile } from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";

import { describeSnapshotFreshness } from "../site/freshness.mjs";
import { renderArchiveSummary } from "./archive-presentation.mjs";
import { documentLinksToExactUrl } from "./document-links.mjs";

const root = process.cwd();
Expand All @@ -23,6 +25,50 @@ describe("canonical project-site links", () => {
});
});

describe("dated archive presentation", () => {
const snapshot = "2026-07-28";

it.each([
["2026-08-11T00:00:00Z", 14, "current", "Current · 14 days old"],
["2026-08-12T00:00:00Z", 15, "refresh-due", "Refresh due · 15 days old"],
["2026-08-27T00:00:00Z", 30, "refresh-due", "Refresh due · 30 days old"],
["2026-08-28T00:00:00Z", 31, "stale", "Stale · 31 days old"],
])("labels the %s threshold honestly", (now, ageDays, state, label) => {
expect(describeSnapshotFreshness(snapshot, Date.parse(now))).toEqual({
ageDays,
state,
label,
});
});

it("rejects malformed dates instead of displaying a misleading age", () => {
expect(() => describeSnapshotFreshness("07/28/2026")).toThrow(/Invalid archive snapshot/);
});

it("renders archive coverage as a narrow-screen list instead of a wide table", () => {
const summary = renderArchiveSummary({
snapshotDate: snapshot,
latestDraw: snapshot,
knownGapCount: 0,
databaseBytesLabel: "1,024",
databaseSha256: "a".repeat(64),
games: [
{
name: "Powerball",
firstDraw: "1992-04-22",
lastDraw: "2026-07-27",
drawCount: 3813,
sessions: 1,
},
],
});

expect(summary).toContain("**Coverage by game**");
expect(summary).toContain("- **Powerball:** 1992-04-22 → 2026-07-27 · 3,813 draws · 1 session");
expect(summary).not.toContain("| Game | Coverage |");
});
});

describe("project-site accessibility contracts", () => {
it("keeps definition-list metric content inside term and description elements", async () => {
const source = await readFile(path.join(root, "site", "index.template.html"), "utf8");
Expand Down
4 changes: 2 additions & 2 deletions scripts/sync-presentation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ for (const claim of requiredClaims) {
if (!expected.includes(claim)) failures.push(`README.md is missing generated claim: ${claim}`);
}
for (const game of data.games) {
const row = `| ${game.name} | ${game.firstDraw} → ${game.lastDraw} |`;
if (!expected.includes(row))
const coverage = `- **${game.name}:** ${game.firstDraw} → ${game.lastDraw} · ${game.drawCount.toLocaleString("en-US")} draws`;
if (!expected.includes(coverage))
failures.push(`README.md is missing current coverage for ${game.name}.`);
}

Expand Down
Loading
Loading