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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ BACKUP_TMP_DIR=./tmp
# B2_OBJECT_LOCK_DAYS=30
# B2_OBJECT_LOCK_MODE=GOVERNANCE

# ─── Manifest signing (tamper-evidence, Ed25519) ──────────────────────────────
# Generate a keypair:
# node -e "const s=require('./src/lib/manifest-signing');const k=s.generateKeyPair();
# require('fs').writeFileSync('signing.key',k.privateKey);
# require('fs').writeFileSync('signing.pub',k.publicKey)"
# BACKUP_SIGNING_KEY= # PEM Ed25519 PRIVATE key → writes manifest.json.sig
# BACKUP_SIGNING_PUBLIC_KEY= # PEM PUBLIC key → verified on restore; also fingerprinted in summary
# BACKUP_REQUIRE_SIGNATURE=false # true = restore aborts if signature missing/invalid

# Optional AES-256-CBC encryption at rest (64 hex chars = 32 bytes). Required
# for both backup (encrypt) and restore (decrypt) — store it in a secrets vault.
# BACKUP_ENCRYPTION_KEY=
Expand Down
40 changes: 39 additions & 1 deletion .github/workflows/monthly-restore-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,50 @@ jobs:
BACKUP_TMP_DIR: /tmp/gh-restore-test
run: |
echo "Starting monthly dry-run restore integrity test..."
echo "RESTORE_START=$(date +%s%3N)" >> $GITHUB_ENV
node src/restore/index.js
echo "RESTORE_END=$(date +%s%3N)" >> $GITHUB_ENV
echo "RESTORE_RESULT=success" >> $GITHUB_ENV

- name: Mark result on failure
if: steps.restore_test.outcome == 'failure'
run: echo "RESTORE_RESULT=failure" >> $GITHUB_ENV
run: |
echo "RESTORE_END=$(date +%s%3N)" >> $GITHUB_ENV
echo "RESTORE_RESULT=failure" >> $GITHUB_ENV

- name: Publish recovery scorecard
if: always()
continue-on-error: true
uses: actions/github-script@v7
env:
RESTORE_RESULT: ${{ env.RESTORE_RESULT }}
RESTORE_START: ${{ env.RESTORE_START }}
RESTORE_END: ${{ env.RESTORE_END }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { buildScorecard } = require(process.env.GITHUB_WORKSPACE + '/src/lib/recovery-scorecard');
const start = parseInt(process.env.RESTORE_START || '0', 10);
const end = parseInt(process.env.RESTORE_END || String(Date.now()), 10);
const sc = buildScorecard({
session: 'latest',
ok: (process.env.RESTORE_RESULT || 'failure') === 'success',
startedAt: start, finishedAt: end,
});
const content = Buffer.from(JSON.stringify(sc, null, 2)).toString('base64');
let sha;
try {
const res = await github.rest.repos.getContent({
owner: context.repo.owner, repo: context.repo.repo, path: 'docs/recovery-scorecard.json'
});
sha = res.data.sha;
} catch (_) {}
await github.rest.repos.createOrUpdateFileContents({
owner: context.repo.owner, repo: context.repo.repo,
path: 'docs/recovery-scorecard.json',
message: 'chore: update recovery scorecard [skip ci]',
content, ...(sha ? { sha } : {})
});

- name: Append restore-test audit entry
if: always()
Expand Down
65 changes: 65 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Publish Docker image (GHCR)

# Builds and publishes the container image to GitHub Container Registry on each
# release (and on manual dispatch), with an SBOM and signed build provenance.

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Image tag to publish (e.g. v5.0.0 or latest)'
required: false
default: 'latest'

permissions:
contents: read
packages: write
id-token: write # sign provenance
attestations: write

jobs:
publish:
runs-on: ubuntu-latest
env:
IMAGE: ghcr.io/${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Derive image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}

- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: true
sbom: true

- name: Attest build provenance
uses: actions/attest-build-provenance@v4
with:
subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [5.0.0] — 2026-07-20

### Added — Trust & verification
- **Signed manifests (Ed25519)** — `src/lib/manifest-signing.js`; `BACKUP_SIGNING_KEY`
signs `manifest.json` → `manifest.json.sig`, verified on restore with
`BACKUP_SIGNING_PUBLIC_KEY` (`BACKUP_REQUIRE_SIGNATURE=true` to enforce).
- **Recovery scorecard** — `src/lib/recovery-scorecard.js`; the monthly restore
drill publishes `docs/recovery-scorecard.json` (last verified restore + RTO),
shown as a README endpoint badge and a dashboard tile.
- **Tamper-evident audit log** — audit entries are now hash-chained with a
`verifyChain()` validator.

### Added — Distribution
- **GitHub Action** (`action.yml`) — `uses: OmarRao/github-gdrive-backup@v5`,
no fork required; outputs a JSON `summary`.
- **GHCR publish workflow** (`publish.yml`) — container image on each release
with SBOM + signed build provenance.

### Changed
- Dashboard System Health adds **Restore Verified** and **Manifest Signature** tiles.
- Docs, `.env.example`, and screenshots updated; SW cache → v6.

### Notes
- New crypto/scorecard/audit logic is unit-tested (16 new tests, 97 total).
No change to existing backup/restore data formats.

---

## [4.1.0] — 2026-07-17

### Performance
Expand Down
23 changes: 23 additions & 0 deletions DEPENDENCY-LICENSE-REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,26 @@ informational only:
purport to relicense third-party dependencies.

Re-run this inventory whenever dependencies change (e.g. after Dependabot updates).

## Known security advisories (open)

| Package | Advisory | Severity | Fix | Status |
|---|---|---|---|---|
| `extract-zip` | [GHSA-jmr9-qjv8-65gv](https://github.com/advisories/GHSA-jmr9-qjv8-65gv) — unvalidated symlink path traversal during extraction | High | **No patched version available** | Tracked follow-up |

**Where it's used:** restore only (`src/restore/index.js`) — extracting a repo's
zip archive into a temp directory on an ephemeral CI runner.

**Exposure & mitigation:**
- The archives extracted are the project's **own** backups (bare git mirrors,
which do not contain symlinks). The risk is a *maliciously crafted* archive in
the storage bucket.
- v5 adds **signed manifests** (`BACKUP_SIGNING_KEY` / `BACKUP_SIGNING_PUBLIC_KEY`)
and `BACKUP_REQUIRE_SIGNATURE=true`, giving tamper-evidence at the manifest
level — restore from a session whose manifest fails verification is aborted.
- Restore only from storage you control, and keep restore runners ephemeral.

**Planned remediation:** replace `extract-zip` with a streaming, path-safe
extractor (rejecting `..`, absolute paths, and symlink entries). Deferred here
because the swap requires installing a replacement package and must be validated
against the restore path; it will be done as a focused, tested change.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![Commercial License](https://img.shields.io/badge/Commercial%20License-available-7c3aed)](COMMERCIAL-LICENSE.md)
[![GitHub Actions](https://img.shields.io/badge/Automated-GitHub%20Actions-1a7f37?logo=github-actions&logoColor=white)](https://github.com/OmarRao/github-gdrive-backup/actions)
[![Backup Status](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FOmarRao%2Fgithub-gdrive-backup%2Fmain%2Fdocs%2Fstatus.json&query=%24.status&label=Backup%20Status&color=22c55e&logo=githubactions&logoColor=white)](https://github.com/OmarRao/github-gdrive-backup/actions)
[![Restore Verified](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FOmarRao%2Fgithub-gdrive-backup%2Fmain%2Fdocs%2Frecovery-scorecard.json)](https://github.com/OmarRao/github-gdrive-backup/actions/workflows/monthly-restore-test.yml)
[![Live Dashboard](https://img.shields.io/badge/Live%20Dashboard-GitHub%20Pages-2563eb?logo=github&logoColor=white)](https://omarrao.github.io/github-gdrive-backup/)
[![Node.js](https://img.shields.io/badge/Node.js-22-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
[![Google Drive](https://img.shields.io/badge/Storage-Google%20Drive-4285F4?logo=googledrive&logoColor=white)](https://drive.google.com/)
Expand Down Expand Up @@ -190,6 +191,11 @@ The dashboard surfaces the latest session's **delta composition** (full / delta
| **First-run onboarding** | Setup checklist gates the dashboard until GitHub + Drive are configured; no stale/upstream data on first login |
| **Fork-aware targeting** | Dashboard auto-derives the target repo from the Pages URL, so each fork shows its own live data (override in Settings) |
| **Dynamic demo data** | Demo stats, graphs, composition & fan-out are all derived from one dataset — always internally consistent |
| **Signed manifests** | Optional Ed25519 signature over `manifest.json` (`BACKUP_SIGNING_KEY`); verified on restore to detect tampering/forgery, not just corruption |
| **Recovery scorecard** | Monthly restore drill publishes a "last verified restore + RTO" scorecard (`docs/recovery-scorecard.json`) surfaced as a README badge and dashboard tile |
| **Tamper-evident audit log** | Hash-chained JSON-lines audit entries — editing or deleting past entries is detectable |
| **GitHub Action** | Use as `uses: OmarRao/github-gdrive-backup@v5` in any workflow — no fork required |
| **Container image (GHCR)** | Published to `ghcr.io/omarrao/github-gdrive-backup` on each release, with SBOM + signed provenance |
| **SBOM generation** | Optional `include_sbom=true` input generates SPDX SBOM via `anchore/sbom-action@v0` |
| **Auto-restore test** | Monthly `monthly-restore-test.yml` dry-run verifies restore integrity; result appended to audit log |
| **PWA / offline** | `manifest.json` + cache-first service worker — install dashboard to home screen, works offline |
Expand Down Expand Up @@ -617,6 +623,55 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, project structure,

---

## Trust & Verification

A backup is only as good as its restore — so v5 makes recoverability and integrity **provable**.

![Trust & Distribution](docs/screenshots/trust-distribution.svg)

- **Signed manifests (Ed25519).** Set `BACKUP_SIGNING_KEY` (a PEM Ed25519 private key) and each session's `manifest.json` is signed to `manifest.json.sig`. On restore, set `BACKUP_SIGNING_PUBLIC_KEY` to verify it; SHA-256 catches *corruption*, the signature catches *tampering/forgery*. Set `BACKUP_REQUIRE_SIGNATURE=true` to abort a restore on a missing/invalid signature. Generate a keypair:
```bash
node -e "const s=require('./src/lib/manifest-signing');const k=s.generateKeyPair();require('fs').writeFileSync('signing.key',k.privateKey);require('fs').writeFileSync('signing.pub',k.publicKey);console.log('wrote signing.key + signing.pub')"
```
- **Recovery scorecard.** The monthly restore drill (`monthly-restore-test.yml`) publishes `docs/recovery-scorecard.json` — *last verified restore + RTO* — shown as the **Restore Verified** badge above and a dashboard tile. "Backups exist" becomes "restores are verified."
- **Tamper-evident audit log.** Audit entries (`src/audit/log.js`) are hash-chained: each carries the previous entry's SHA-256, so editing or deleting history is detectable via `verifyChain()`.

---

## Use as a GitHub Action

No fork required — compose it into any workflow:

```yaml
- uses: OmarRao/github-gdrive-backup@v5
with:
github-token: ${{ secrets.GH_BACKUP_TOKEN }}
gdrive-folder-id: ${{ secrets.GDRIVE_FOLDER_ID }}
google-client-secret: ${{ secrets.GOOGLE_CLIENT_SECRET }}
google-token: ${{ secrets.GOOGLE_TOKEN }}
incremental-mode: delta # optional
mirror-targets: s3,b2 # optional 3-2-1 fan-out
signing-key: ${{ secrets.BACKUP_SIGNING_KEY }} # optional
```

See [`action.yml`](action.yml) for all inputs. The action outputs a JSON `summary` (totals, delta composition, fan-out, signature).

---

## Container image (GHCR)

Published to the GitHub Container Registry on each release with an SBOM and signed build provenance:

```bash
docker run --rm \
-e GITHUB_TOKEN=ghp_... -e GITHUB_USER=you \
-e GDRIVE_FOLDER_ID=... \
-v "$PWD/credentials:/app/credentials" \
ghcr.io/omarrao/github-gdrive-backup:v5 backup
```

---

## Licensing

**Copyright © 2026 Omar Rao.**
Expand Down
32 changes: 32 additions & 0 deletions SUPPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Support

Thanks for using **github-gdrive-backup**.

## Community support (free, AGPL-3.0)

- **Bugs & feature requests:** open a [GitHub Issue](https://github.com/OmarRao/github-gdrive-backup/issues).
- **Questions & ideas:** use [GitHub Discussions](https://github.com/OmarRao/github-gdrive-backup/discussions) if enabled, or an Issue.
- **Security reports:** see [SECURITY.md](SECURITY.md) — please do **not** file public issues for vulnerabilities.

Community support is best-effort, provided by the maintainer and other users under
the terms of the [AGPL-3.0 license](LICENSE). There is no SLA.

## Commercial support & licensing

The project is dual-licensed (see [COMMERCIAL-LICENSE.md](COMMERCIAL-LICENSE.md)).
A commercial license is required for closed-source, internal-proprietary,
SaaS/hosted, integration, redistribution, resale, distributor, or white-label use.

Commercial engagements can include (by separate written agreement):

- a commercial license grant (no AGPL source-disclosure obligation);
- priority support and defined response targets;
- deployment/onboarding assistance;
- private feature or integration work.

To discuss commercial licensing or support, contact **Omar Rao** —
`omarsrao@gmail.com`. Please include your organization, intended use, deployment
model, and scale so a suitable proposal can be prepared.

> This document is informational. Any commercial terms arise solely from a
> separate written agreement, and nothing here is legal advice.
Loading
Loading