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
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
collector:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm run lint
- run: npm run test
55 changes: 55 additions & 0 deletions .github/workflows/release-security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Release Security

on:
push:
tags:
- 'v*.*.*'

jobs:
supply-chain:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
attestations: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Generate SBOM (CycloneDX)
uses: CycloneDX/gh-node-module-generatebom@v1
with:
path: .
output: sbom.cdx.json
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.cdx.json
- name: Build container
run: docker build -f apps/collector/Dockerfile -t ghcr.io/${{ github.repository }}/collector:${{ github.ref_name }} .
- name: Trivy scan image
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: ghcr.io/${{ github.repository }}/collector:${{ github.ref_name }}
severity: CRITICAL,HIGH
exit-code: '1'
- name: Login GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push image
run: docker push ghcr.io/${{ github.repository }}/collector:${{ github.ref_name }}
- name: Install cosign
uses: sigstore/cosign-installer@v3.7.0
- name: Sign image with Cosign keyless
run: cosign sign --yes ghcr.io/${{ github.repository }}/collector:${{ github.ref_name }}
- name: Attest build provenance
uses: actions/attest-build-provenance@v1
with:
subject-name: ghcr.io/${{ github.repository }}/collector
subject-digest: sha256:${{ github.sha }}
8 changes: 8 additions & 0 deletions .meridian/policies/branch_protection.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package meridian.branch

deny[msg] {
input.action == "push"
input.branch == "main"
not input.metadata.via_pull_request
msg := "Direct push to main is not allowed"
}
3 changes: 3 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
registry=https://registry.npmjs.org/
fund=false
audit=false
5 changes: 5 additions & 0 deletions .well-known/security.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Contact: mailto:security@meridian.io
Expires: 2027-01-01T00:00:00.000Z
Preferred-Languages: en, pt-BR
Policy: https://meridian.io/security
Hiring: https://meridian.io/careers
28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.PHONY: setup dev test lint up down load-test up-minimal validate-local

setup:
npm install

dev:
npm run dev

test:
npm run test

lint:
npm run lint

up:
docker compose up -d --build

down:
docker compose down -v

load-test:
k6 run tests/load/k6-script.js

up-minimal:
docker compose -f docker-compose.minimal.yml up -d --build

validate-local:
./scripts/validate-local.sh
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,55 @@
# Meridian
The Fixed Point of Truth for Enterprise Engineering Governance

**The Fixed Point of Truth for Enterprise Engineering**

Meridian é uma plataforma de governança e compliance para Git enterprise com trilha de auditoria imutável, policy-as-code e inteligência de conformidade.

## Estado atual (entregável executável)

- Collector funcional em TypeScript/Fastify (`apps/collector`)
- Policy Engine inicial (`apps/policy-engine`) com avaliação de violações críticas
- Persistência de eventos em PostgreSQL append-only com hash chain e RLS por tenant
- Endpoints ativos:
- `POST /webhooks/github`
- `POST /webhooks/gitlab`
- `GET /audit/integrity?tenant_id=...`
- `GET /audit/export?tenant_id=...&from=...&to=...` (includes `key_id`, signature metadata)
- `GET /metrics`
- Verificação offline de export via CLI:
- `node apps/cli/meridian.js verify --bundle audit-export.json --key <export_signing_key>`
- Ed25519 mode: `node apps/cli/meridian.js verify --bundle audit-export.json --public-key ./export-public.pem`

## Quickstart

```bash
make setup
make up
```

Collector em `http://localhost:8080` e Policy Engine em `http://localhost:8081`.

## Segurança e Trust Signals

- Row-Level Security por tenant no banco
- Chaves/segredos por tenant via configuração (`TENANT_GITHUB_SECRETS`, `TENANT_GITLAB_TOKENS`, `TENANT_API_KEYS`)
- Release security pipeline com SBOM, Trivy, Cosign e provenance attestation
- Dependabot semanal para npm e GitHub Actions
- Responsible disclosure policy + `.well-known/security.txt`

## Estrutura-chave

- Código: `apps/collector/`, `apps/policy-engine/`, `apps/cli/`
- SQL append-only + RLS: `apps/collector/sql/001_init.sql`
- CI/CD: `.github/workflows/ci.yml`, `.github/workflows/release-security.yml`
- OpenAPI: `docs/api/openapi.yaml`
- Governança: `docs/governance/`
- Segurança: `docs/security/`
- Compliance: `docs/compliance/`
- Support/LTS: `docs/support/`
- Procurement: `docs/procurement/`
- Pilot package: `docs/pilot/`
- Integrações enterprise: `docs/integrations/`
- Arquitetura de referência: `docs/architecture/reference-deployment-aws.md`, `docs/architecture/reference-deployment-azure.md`
- Performance e resiliência: `tests/load/k6-script.js`, `tests/resilience/`, `reports/`
- Validação local ponta a ponta: `make validate-local` (gera `reports/local-validation-report.md`)
- Stack mínima (Postgres + Collector): `make up-minimal`
11 changes: 11 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Security Policy

## Responsible Disclosure
- Contact: security@meridian.io
- Preferred encryption: PGP on request
- Acknowledgement target: 72 hours
- Initial triage target: 5 business days

## Bug Bounty (Pilot)
- Scope: Collector API endpoints and authentication/tenant-isolation controls
- Safe harbor for good-faith research within scope
66 changes: 66 additions & 0 deletions apps/cli/meridian.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env node
import fs from 'node:fs';
import crypto from 'node:crypto';

function canonicalize(input) {
if (input === null || typeof input !== 'object') return JSON.stringify(input);
if (Array.isArray(input)) return `[${input.map((v) => canonicalize(v)).join(',')}]`;
const keys = Object.keys(input).sort();
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(input[k])}`).join(',')}}`;
}

function sha256(value) {
return crypto.createHash('sha256').update(value).digest('hex');
}

function hmacSha256(value, key) {
return crypto.createHmac('sha256', key).update(value).digest('hex');
}

function usage() {
console.log('Usage: meridian verify --bundle <path> [--key <signing_key>] [--public-key <pem_path>]');
}

const args = process.argv.slice(2);
if (args[0] !== 'verify') {
usage();
process.exit(1);
}

const bundleFlag = args.indexOf('--bundle');
if (bundleFlag === -1 || !args[bundleFlag + 1]) {
usage();
process.exit(1);
}

const path = args[bundleFlag + 1];
const keyFlag = args.indexOf('--key');
const publicKeyFlag = args.indexOf('--public-key');
const key = (keyFlag !== -1 ? args[keyFlag + 1] : undefined) || process.env.EXPORT_SIGNING_KEY || 'dev-export-signing-key';
const publicKeyPath = publicKeyFlag !== -1 ? args[publicKeyFlag + 1] : undefined;

const data = JSON.parse(fs.readFileSync(path, 'utf8'));
const { bundle_hash, signature, signature_algorithm, key_id, key_provider, key_reference, public_key, ...unsignedBundle } = data;
const recomputedHash = sha256(canonicalize(unsignedBundle));
const hashValid = recomputedHash === bundle_hash;

let signatureValid = false;
if ((signature_algorithm ?? '').toUpperCase() === 'ED25519') {
const providedPublic = publicKeyPath ? fs.readFileSync(publicKeyPath, 'utf8') : undefined;
const publicKey = providedPublic || data.public_key || process.env.EXPORT_PUBLIC_KEY_PEM;
if (publicKey) {
signatureValid = crypto.verify(null, Buffer.from(recomputedHash), publicKey, Buffer.from(signature, 'base64'));
}
} else {
signatureValid = hmacSha256(recomputedHash, key) === signature;
}

console.log(JSON.stringify({
format_version: data.format_version,
key_id,
hash_valid: hashValid,
signature_valid: signatureValid,
signature_algorithm: signature_algorithm ?? 'HMAC-SHA256'
}, null, 2));

if (!hashValid || !signatureValid) process.exit(2);
8 changes: 8 additions & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@meridian/cli",
"version": "0.1.0",
"type": "module",
"bin": {
"meridian": "./meridian.js"
}
}
12 changes: 12 additions & 0 deletions apps/collector/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
COPY apps/collector/package.json apps/collector/package.json
COPY apps/policy-engine/package.json apps/policy-engine/package.json
COPY apps/cli/package.json apps/cli/package.json
RUN npm install
COPY . .
RUN npm run -w apps/collector build
USER node
EXPOSE 8080
CMD ["npm", "run", "-w", "apps/collector", "start"]
26 changes: 26 additions & 0 deletions apps/collector/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@meridian/collector",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"test": "vitest run",
"lint": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@fastify/sensible": "^5.6.0",
"fastify": "^4.28.1",
"pg": "^8.12.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.10.2",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"vitest": "^2.1.8",
"@types/pg": "^8.11.11"
}
}
37 changes: 37 additions & 0 deletions apps/collector/sql/001_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
CREATE TABLE IF NOT EXISTS audit_events (
id UUID PRIMARY KEY,
tenant_id TEXT NOT NULL,
provider TEXT NOT NULL,
event_type TEXT NOT NULL,
previous_hash TEXT NOT NULL,
event_hash TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_audit_events_tenant_created_at
ON audit_events (tenant_id, created_at DESC);

CREATE OR REPLACE FUNCTION forbid_audit_events_mutation()
RETURNS trigger AS $$
BEGIN
RAISE EXCEPTION 'audit_events is append-only';
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS trg_no_update_delete ON audit_events;
CREATE TRIGGER trg_no_update_delete
BEFORE UPDATE OR DELETE ON audit_events
FOR EACH ROW EXECUTE FUNCTION forbid_audit_events_mutation();

ALTER TABLE audit_events ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS tenant_isolation_select ON audit_events;
CREATE POLICY tenant_isolation_select ON audit_events
FOR SELECT
USING (tenant_id = current_setting('app.tenant_id', true));

DROP POLICY IF EXISTS tenant_isolation_insert ON audit_events;
CREATE POLICY tenant_isolation_insert ON audit_events
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id', true));
27 changes: 27 additions & 0 deletions apps/collector/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
function parseJsonMap(input: string | undefined): Record<string, string> {
if (!input) return {};
try {
const parsed = JSON.parse(input) as Record<string, string>;
return parsed ?? {};
} catch {
return {};
}
}

export const config = {
port: Number(process.env.PORT ?? 8080),
host: process.env.HOST ?? '0.0.0.0',
databaseUrl: process.env.DATABASE_URL ?? 'postgres://meridian:meridian@localhost:5432/meridian',
githubSecret: process.env.GITHUB_WEBHOOK_SECRET ?? 'dev-github-secret',
gitlabToken: process.env.GITLAB_WEBHOOK_TOKEN ?? 'dev-gitlab-token',
exportSigningAlgorithm: process.env.EXPORT_SIGNING_ALGORITHM ?? 'ed25519',
exportSigningKeyId: process.env.EXPORT_SIGNING_KEY_ID ?? 'local-dev-key-v1',
exportSigningKey: process.env.EXPORT_SIGNING_KEY ?? 'dev-export-signing-key',
exportPrivateKeyPem: process.env.EXPORT_PRIVATE_KEY_PEM,
exportPublicKeyPem: process.env.EXPORT_PUBLIC_KEY_PEM,
keyProvider: process.env.KEY_PROVIDER ?? 'env',
keyReference: process.env.KEY_REFERENCE ?? 'local://export-signing-key',
tenantGithubSecrets: parseJsonMap(process.env.TENANT_GITHUB_SECRETS),
tenantGitlabTokens: parseJsonMap(process.env.TENANT_GITLAB_TOKENS),
tenantApiKeys: parseJsonMap(process.env.TENANT_API_KEYS)
};
Loading