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
19 changes: 19 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: ts-stack security-extended analysis

queries:
- uses: security-extended

# Every ignored path is a checked-in generated boundary owned in
# governance/repository-health/projects.json. Repository controls require this
# list to match that registry exactly.
paths-ignore:
- conformance/generated/**
- docs-site/public/assets/asyncapi/**
- packages/verifast/src/wasm/bdk-core.*
- packages/wallet/wallet-toolbox/docs/open-rpc/index.html
- infra/uhrp-server-basic/src/security/edgePolicy.ts
- infra/uhrp-server-cloud-bucket/src/security/edgePolicy.ts
- infra/message-box-server/src/security/edgePolicy.ts
- infra/chaintracks-server/src/security/edgePolicy.ts
- packages/overlays/overlay-express/src/security/edgePolicy.ts
- packages/wallet/wallet-toolbox/src/storage/remoting/edgePolicy.ts
53 changes: 53 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CodeQL

on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '17 8 * * 1'
workflow_dispatch:

concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Keep default setup authoritative while this workflow is reviewed. The
# repository variable is enabled only after merge, immediately before
# default setup is disabled and this workflow is dispatched on main.
if: vars.CODEQL_ADVANCED_ENABLED == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
actions: read
contents: read
packages: read
security-events: write
strategy:
fail-fast: false
matrix:
language:
- actions
- javascript-typescript
- python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Initialize CodeQL
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
languages: ${{ matrix.language }}
build-mode: none
config-file: ./.github/codeql/codeql-config.yml

- name: Analyze
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
category: /language:${{ matrix.language }}
14 changes: 12 additions & 2 deletions docs/reference/repository-health.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ and analysis treatment. Generated output can be excluded narrowly only when
this relationship is explicit. Product source and tests must not be
reclassified as generated to reduce quality findings.

`.github/codeql/codeql-config.yml` is the executable CodeQL boundary. Its
`paths-ignore` entries must exactly match the owned generated artifacts above.
The advanced workflow keeps JavaScript/TypeScript, Python, and GitHub Actions
analysis on pull requests, `main` pushes, and a weekly schedule with the
`security-extended` suite. A repository-control test rejects any loss of those
languages, events, permissions, query coverage, or required check names.
`CODEQL_ADVANCED_ENABLED` is the repository-level cutover gate: keep it `true`
after advanced setup is activated. It exists only to prevent default and
advanced analysis from uploading concurrently during the migration PR.

`governance/repository-health/exceptions.json` is the only registry for temporary
exceptions. Its schema is in `exception.schema.json`. Every entry requires an
owner, rationale, evidence, creation date, review deadline, and objective
Expand Down Expand Up @@ -90,8 +100,8 @@ checks:
1. discovery matches the exact 37-project inventory;
2. names, owners, profiles, criticality, targets, privacy, and release methods
are internally consistent;
3. generated artifact boundaries have valid owners, sources, generators, and
narrow review/analysis policies;
3. generated artifact boundaries have valid owners, sources, generators, narrow
review/analysis policies, and exact CodeQL exclusion coverage;
4. published package versions match the recorded baseline;
5. exception records are owned, structurally valid, and unexpired;
6. current package-contract findings exactly match the ratcheted snapshot; and
Expand Down
2 changes: 1 addition & 1 deletion governance/repository-health/projects.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"reviewPolicy": "Review the specification and renderer changes; verify regeneration is deterministic in the docs build."
},
{
"path": "packages/verifast/src/wasm/bdk-core.{mjs,browser.mjs,umd.js,wasm,umd.wasm,d.ts,browser.d.ts}",
"path": "packages/verifast/src/wasm/bdk-core.*",
"owner": "ts-stack-maintainers",
"sourceInputs": [
"packages/verifast/README.md#reproducible-wasm-build"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,70 @@ describe('ExpressTransport configuration', () => {
expect(res.status).not.toHaveBeenCalled()
})

it('dispatches only the exact BRC-104 path to the public handshake', async () => {
const transport = new ExpressTransport()
transport.peer = {} as any
const handshake = jest
.spyOn(transport as any, 'handleWellKnownAuth')
.mockResolvedValue(undefined)
const general = jest
.spyOn(transport as any, 'handleGeneralMessage')
.mockImplementation(() => {})
const unauthenticated = jest
.spyOn(transport as any, 'handleUnauthenticated')
.mockImplementation(() => {})
const res = {} as any
const next = jest.fn()

await transport.handleIncomingRequest(
{ path: '/.well-known/auth', headers: {} } as any,
res,
next
)
expect(handshake).toHaveBeenCalledTimes(1)
expect(general).not.toHaveBeenCalled()
expect(unauthenticated).not.toHaveBeenCalled()

for (const path of [
'/.well-known/auth/extra',
'/.well-known/authentication',
'/.well-known/Auth',
'/protected'
]) {
await transport.handleIncomingRequest({ path, headers: {} } as any, res, next)
}
expect(handshake).toHaveBeenCalledTimes(1)
expect(general).not.toHaveBeenCalled()
expect(unauthenticated).toHaveBeenCalledTimes(4)
})

it('requires an auth request ID before using signed general-message dispatch', async () => {
const transport = new ExpressTransport()
transport.peer = {} as any
const handshake = jest
.spyOn(transport as any, 'handleWellKnownAuth')
.mockResolvedValue(undefined)
const general = jest
.spyOn(transport as any, 'handleGeneralMessage')
.mockImplementation(() => {})
const unauthenticated = jest
.spyOn(transport as any, 'handleUnauthenticated')
.mockImplementation(() => {})

await transport.handleIncomingRequest(
{
path: '/protected',
headers: { 'x-bsv-auth-request-id': 'signed-request-id' }
} as any,
{} as any,
jest.fn()
)

expect(general).toHaveBeenCalledTimes(1)
expect(handshake).not.toHaveBeenCalled()
expect(unauthenticated).not.toHaveBeenCalled()
})

it('stores attacker-controlled request IDs in prototype-safe maps', async () => {
const transport = new ExpressTransport()
transport.peer = {
Expand Down
8 changes: 7 additions & 1 deletion packages/middleware/auth-express-middleware/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export type { LogLevel } from './authMiddlewareHelpers.js'
export { isLogLevelEnabled, getLogMethod } from './authMiddlewareHelpers.js'
export { writeBodyToWriter } from './authMiddlewareHelpers.js'

const WELL_KNOWN_AUTH_PATH = '/.well-known/auth'

export interface AuthRequest extends Request {
auth?: {
identityKey: PubKeyHex
Expand Down Expand Up @@ -421,7 +423,11 @@ export class ExpressTransport implements Transport {
this.log('error', 'No Peer set in ExpressTransport! Cannot handle request.')
throw new Error('You must set a Peer before you can handle incoming requests!')
}
if (req.path === '/.well-known/auth') {
// BRC-104 authentication begins on this intentionally public handshake
// endpoint, before a session can exist. This is protocol dispatch, not a
// protected-route authorization check. Every other request still takes
// the signed general-message path or the explicit unauthenticated policy.
if (req.path === WELL_KNOWN_AUTH_PATH) {
await this.handleWellKnownAuth(req, res, next, onCertificatesReceived)
} else if (req.headers['x-bsv-auth-request-id']) {
this.handleGeneralMessage(req, res, next)
Expand Down
62 changes: 62 additions & 0 deletions scripts/codeql-config.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import test from 'node:test'
import { REPOSITORY_ROOT } from './repository-health.mjs'

const CODEQL_CONFIG_PATH = join(REPOSITORY_ROOT, '.github/codeql/codeql-config.yml')
const CODEQL_WORKFLOW_PATH = join(REPOSITORY_ROOT, '.github/workflows/codeql.yml')
const PROJECTS_PATH = join(REPOSITORY_ROOT, 'governance/repository-health/projects.json')
const CODEQL_ACTION_SHA = 'e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81'

function readIndentedList (source, key, indentation = 0) {
const lines = source.split('\n')
const prefix = `${' '.repeat(indentation)}${key}:`
const start = lines.findIndex(line => line === prefix)
assert.notEqual(start, -1, `${key} list is missing`)

const values = []
const itemIndentation = indentation + 2
for (const line of lines.slice(start + 1)) {
if (line.trim() === '' || line.trimStart().startsWith('#')) continue
const currentIndentation = line.length - line.trimStart().length
if (currentIndentation <= indentation) break
if (currentIndentation === itemIndentation && line.trimStart().startsWith('- ')) {
values.push(line.trimStart().slice(2))
}
}
return values
}

test('CodeQL scans the owned source boundary with the security-extended suite', () => {
const config = readFileSync(CODEQL_CONFIG_PATH, 'utf8')
const registry = JSON.parse(readFileSync(PROJECTS_PATH, 'utf8'))
const ownedGeneratedPaths = registry.generatedArtifacts
.filter(artifact => artifact.analysisPolicy === 'exclude-generated')
.map(artifact => artifact.path)
.sort()

assert.deepEqual(readIndentedList(config, 'queries'), ['uses: security-extended'])
assert.deepEqual(readIndentedList(config, 'paths-ignore').sort(), ownedGeneratedPaths)
})

test('advanced CodeQL preserves languages, events, permissions, and required check names', () => {
const workflow = readFileSync(CODEQL_WORKFLOW_PATH, 'utf8')

assert.deepEqual(readIndentedList(workflow, 'language', 8), [
'actions',
'javascript-typescript',
'python'
])
assert.match(workflow, /^ push:\n branches: \[main\]$/m)
assert.match(workflow, /^ pull_request:\n branches: \[main\]$/m)
assert.match(workflow, /^ schedule:\n - cron: '.+'$/m)
assert.match(workflow, /^ workflow_dispatch:$/m)
assert.match(workflow, /^ name: Analyze \(\$\{\{ matrix\.language \}\}\)$/m)
assert.match(workflow, /^ if: vars\.CODEQL_ADVANCED_ENABLED == 'true'$/m)
assert.match(workflow, /^ security-events: write$/m)
assert.match(workflow, /^ build-mode: none$/m)
assert.match(workflow, /^ config-file: \.\/\.github\/codeql\/codeql-config\.yml$/m)
assert.match(workflow, new RegExp(`github/codeql-action/init@${CODEQL_ACTION_SHA}`))
assert.match(workflow, new RegExp(`github/codeql-action/analyze@${CODEQL_ACTION_SHA}`))
})
Loading