docs: record upstream security audit (Mission 01)

- docs/AUDITORIA_BASE_2GT.md: full audit of upstream 1a4b635 (v0.24.5-beta)
  - 0 critical, 1 high (SEC-002: no per-site RBAC), 4 medium, 5 low, 5 informational
  - Verdict: APROVADO COM CORREÇÕES OBRIGATÓRIAS
  - First read-only RB: authorized (SIM)
- test: add regression coverage proving SEC-001 (no JWT revocation) and
  SEC-002 (site scoping has no per-user authorization component)
- docs/SISTEMA_ATUAL.md: record confirmed facts (RBAC scope, Change Guard
  coverage, RouterOS TLS verification) surfaced by the audit

No production code changed — audit-only per mission scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-09-08 18:32:03 -03:00
co-authored by Claude Sonnet 5
parent 7a4979e0e1
commit 5b926d637b
3 changed files with 286 additions and 2 deletions
@@ -0,0 +1,56 @@
/**
* Regression coverage for findings recorded in docs/AUDITORIA_BASE_2GT.md.
*
* These tests prove the *current* upstream behavior with evidence, rather than
* asserting an opinion. They are not fixes — see the audit report for
* classification, impact and recommendation of each finding.
*/
import { signToken, verifyToken, AuthPayload } from '../auth';
import { resolveSiteId } from '../../utils/siteScope';
// ── SEC-001: JWTs are not invalidated by a password/role change ─────────────
//
// signToken/verifyToken never consult the database — a JWT is self-contained
// and valid for the full 24h `expiresIn` window regardless of what happens to
// the account afterward. There is no token version, jti, or revocation list
// consulted anywhere in requireAuth (see middleware/auth.ts). This test proves
// the mechanism: a token minted for a role has no way to be invalidated short
// of rotating JWT_SECRET (which invalidates every session at once).
describe('SEC-001: JWT sessions carry no revocation mechanism', () => {
it('a token remains valid and keeps its original role after being issued, ' +
'with nothing in verifyToken that could observe a later password/role change', () => {
const payload: AuthPayload = { userId: 42, username: 'ops', role: 'admin' };
const token = signToken(payload);
// Simulate time passing / the account changing in the DB — verifyToken
// takes only the token, so it cannot know about that change.
const decoded = verifyToken(token);
expect(decoded.role).toBe('admin');
expect(decoded.userId).toBe(42);
// No exp check failure, no DB round-trip: the token is still authoritative
// for up to 24h from signing, whatever happened to the account since.
});
});
// ── SEC-002: "sites" are a display filter, not an authorization boundary ────
//
// resolveSiteId is the entire enforcement surface for site scoping: it turns
// a client-supplied header/query value into a SQL predicate. It has no
// knowledge of req.user and no lookup of which sites a user may access —
// confirmed by its signature (raw value in, id-or-null out) and by grep: no
// route file cross-references req.user with a site membership table (none
// exists in the schema). Any authenticated user (any role) can omit the
// header entirely to see every site's devices.
describe('SEC-002: site scoping has no per-user authorization component', () => {
it('resolves any syntactically valid site id with no user/session context ' +
'available to the function at all', () => {
// Note the signature: no user, no session, no membership check — just a
// string. This is the one function that decides "which fleet subset",
// and it never sees who is asking.
expect(resolveSiteId('1')).toBe(1);
expect(resolveSiteId('2')).toBe(2);
expect(resolveSiteId(undefined)).toBeNull(); // unscoped = whole fleet
expect(resolveSiteId('all')).toBeNull(); // explicit whole fleet
});
});