Security Testing Policy
Security Testing Policy
ProjectUpdated/2026-04-29 for ADR-021 path realignment. Seedocs/architecture/ADR-021-bilko-blueprint-section-15-realignment.md.
Organization:
ALAI Holding ASBilko —DropBalkanPaymentAccountingAppSaaS Policy Number: POL-SEC-TEST-001 Version: 1.0 Date: 2026-02-23 Author: CTO / SecurityArchitectEngineer Status: Draft Reviewers:CISO,Engineering Lead,CTONext Review:2027-02-23DPO Classification: Confidential
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-23 | Initial |
1. Purpose & Scope
Purpose: This policy defines the security testing methodology,requirements, tools, frequency,schedule, and remediationacceptance requirements for all systems operated by ALAI Holding AScriteria for the DropBilko paymentplatform. app.Bilko handles regulated financial data (tax IDs, IBAN, accounting records) across three jurisdictions. Security testing is mandatorymandatory, —not no system goes to production without completing applicable security tests.optional.
Regulatory basis:
IKT-forskriften (FOR-2003-05-21-630) §§ 5-6 — ICT security verificationDORA (EU) 2022/2554 Art. 24-25 — Digital operational resilience testingFinanstilsynet licensing requirements — penetration test before production launchGDPR Art. 32(1)(d) — regular testing of technical measures
Scope:
- All
productionBilkoAPIsapplications — Express API (apps/api-express/), Next.js frontend (apps/web/), database layer (Prisma + PostgreSQL), andendpoints (getdrop.no,api.getdrop.no) All AWS infrastructure (App Runner, S3, KMS, Secrets Manager)All CI/CD pipelines and build systemsAll third-partyexternal integrations (BankID,SEF,Sumsub,HR-FISK).OpenTheBankingKotlin/Ktorpartners)Developer workstations handling Confidential or Restricted data
Policy Owner: CISObackend ([email protected])apps/api/) Operationalis Owner:covered Securityseparately teamas +it Engineeringmatures Lead(MC #5125).
2. Security Testing MethodologyPyramid
Approach: Shift-left security — testing integrated throughout development, not bolted on at the end.
Testing layers:
Developmentgraph PhaseTD
+--subgraph AUTOMATED["Automated (runs every CI pipeline)"]
SAST["SAST\nESLint Security Rules\nTypeScript strict mode\nnpm audit\nSnyk SCA"]
UNIT["Security Unit securityTests\nVitest\nRBAC testsmatrix —tests\nOrg Vitestisolation tests\nEncryption tests\nVAT accuracy tests"]
INT["Integration Tests\nVitest + Supertest\nAuth flow tests\nJWT validation\nRate limiting tests"]
end
subgraph PERIODIC["Periodic (current:scheduled)"]
20+DAST["DAST\nOWASP security-specific tests)ZAP\nMonthly + pre-release"]
E2E["Security E2E\nPlaywright\nCross-tenant boundary tests\nPrivilege escalation tests"]
end
subgraph MANUAL["Manual (scheduled)"]
PENTEST["Penetration Test\nExternal vendor\nAnnual"]
REVIEW["Security Code Review\nPre-merge (security-sensitive PRs)\nArchitecture review quarterly"]
end
UNIT --> SCAINT — npm audit (every commit, automated)
+-- Secret scanning — detect leaked credentials (every commit)
Build / CI Phase
+-- SAST — static code analysis (every PR — planned Phase 2)
+-- SCA — dependency vulnerability check (every PR)
+-- Secret scanning — full scan (every PR)
Deployment Phase
+--> DAST — OWASP ZAP against staging (planned — Phase 3)
+--> API security scan — endpoint fuzzing (planned — Phase 3)
Operational Phase
+-- Vulnerability assessment — external attack surface (quarterly)
+-- Penetration test — manual expert testing (before Phase 3 launch)
+-- Post-incident review — regression tests after any security incidentPENTEST
3. CurrentAutomated Security TestTesting Coverage(CI/CD)
Every push to main and every pull request triggers:
3.1 VitestStatic UnitAnalysis Security Tests(SAST)
Status: Implemented — 20+ security-specific tests
Source: src/drop-app/src/__tests__/
eslint-plugin-security |
Any level finding blocks merge |
|
any that could bypass validation |
Build failure blocks merge |
|
npm audit --audit-level=high |
HIGH or CRITICAL CVEs block merge |
|
| CRITICAL ||
| ||
| ||
| ||
| ||
| ||
| ||
| ||
| ||
| ||
| ||
| ||
| ||
| ||
| ||
| ||
| ||
merge |
3.2 Running the Security TestUnit SuiteTests (Vitest)
Location: apps/api-express/src/__tests__/security/
Required test suites:
RBAC Matrix Tests
// Every permission combination must be explicitly tested
describe('RBAC — Invoice access', () => {
const roles = ['owner', 'admin', 'accountant', 'viewer']
test.each([
['owner', 'create', true],
['admin', 'create', true],
['accountant', 'create', true],
['viewer', 'create', false],
['owner', 'delete', true],
['admin', 'delete', true],
['accountant', 'delete', false],
['viewer', 'delete', false],
])('role=%s action=%s expected=%s', async (role, action, expected) => {
const token = signTestJWT({ role, org: 'org-1' })
const res = await request(app).post(`/api/invoices`).set('Authorization', `Bearer ${token}`)
// check response matches expected
})
})
Organization Isolation Tests (Multi-Tenant Critical)
describe('Org isolation — no cross-tenant data leak', () => {
let org1Token: string
let org2InvoiceId: string
beforeAll(async () => {
// Setup two orgs with data
org1Token = signTestJWT({ org: 'org-1', role: 'owner' })
const org2Token = signTestJWT({ org: 'org-2', role: 'owner' })
// Create invoice in org-2
const res = await request(app)
.post('/api/invoices')
.set('Authorization', `Bearer ${org2Token}`)
.send(validInvoicePayload)
org2InvoiceId = res.body.id
})
test('org-1 cannot read org-2 invoice', async () => {
const res = await request(app)
.get(`/api/invoices/${org2InvoiceId}`)
.set('Authorization', `Bearer ${org1Token}`)
expect(res.status).toBe(404) // NOT 403 — don't reveal existence
})
test('org-1 list does not include org-2 data', async () => {
const res = await request(app).get('/api/invoices').set('Authorization', `Bearer ${org1Token}`)
const ids = res.body.data.map((i: any) => i.id)
expect(ids).not.toContain(org2InvoiceId)
})
})
Field Encryption Tests
describe('Field encryption — L4 Restricted', () => {
test('PIB stored encrypted in DB', async () => {
const testPIB = '123456789' // fake PIB
// Create contact with PIB
await request(app)
.post('/api/contacts')
.set('Authorization', `Bearer ${ownerToken}`)
.send({ name: 'Test', taxId: testPIB, type: 'RS' })
// Read raw DB value — should not be plaintext
const raw = await prisma.$queryRaw`
SELECT "taxId" FROM "Contact" WHERE name = 'Test'
`
expect(raw[0].taxId).not.toBe(testPIB)
expect(raw[0].taxId).toMatch(/^[A-Za-z0-9+/]+=*:[A-Za-z0-9+/]+=*:[A-Za-z0-9+/]+=*$/)
// Should be base64:base64:base64 format (iv:authTag:ciphertext)
})
test('decrypted PIB matches original on read', async () => {
const res = await request(app).get('/api/contacts').set('Authorization', `Bearer ${ownerToken}`)
const contact = res.body.data.find((c: any) => c.name === 'Test')
expect(contact.taxId).toBe('123456789')
})
})
VAT Accuracy Tests (Financial Compliance)
describe('VAT calculation accuracy', () => {
test('RS: VAT 20% on standard goods (NUMERIC precision)', () => {
const net = new Decimal('100.00')
const vatAmount = net.mul('0.20')
const gross = net.plus(vatAmount)
expect(vatAmount.toString()).toBe('20.00')
expect(gross.toString()).toBe('120.00')
})
test('HR: VAT 25% (EUR since Jan 2024)', () => {
const net = new Decimal('100.00')
const gross = net.mul('1.25')
expect(gross.toString()).toBe('125.00')
})
test('BA: VAT 17% (UIO standard)', () => {
const net = new Decimal('100.00')
const gross = net.mul('1.17')
expect(gross.toString()).toBe('117.00')
})
test('No float drift on invoice totals', () => {
// Known JS float bug: 0.1 + 0.2 !== 0.3
const line1 = new Decimal('0.10')
const line2 = new Decimal('0.20')
expect(line1.plus(line2).toString()).toBe('0.30')
// Contrast: expect(0.1 + 0.2).toBe(0.3) would FAIL
})
})
Authentication Tests
describe('Auth — JWT security', () => {
test('expired access token returns 401', async () => {
const expiredToken = signTestJWT({ exp: Math.floor(Date.now() / 1000) - 1 })
const res = await request(app)
.get('/api/invoices')
.set('Authorization', `Bearer ${expiredToken}`)
expect(res.status).toBe(401)
})
test('tampered token returns 401', async () => {
const validToken = signTestJWT({ role: 'viewer' })
// Tamper: change role claim in payload
const parts = validToken.split('.')
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString())
payload.role = 'owner' // attempt privilege escalation
parts[1] = Buffer.from(JSON.stringify(payload)).toString('base64url')
const tamperedToken = parts.join('.')
const res = await request(app)
.delete('/api/invoices/any-id')
.set('Authorization', `Bearer ${tamperedToken}`)
expect(res.status).toBe(401)
})
test('rate limiting: 6th auth attempt in 15min returns 429', async () => {
for (let i = 0; i < 5; i++) {
await request(app).post('/api/auth/login').send({ email: 'x', password: 'wrong' })
}
const res = await request(app).post('/api/auth/login').send({ email: 'x', password: 'wrong' })
expect(res.status).toBe(429)
})
})
3.3 Dependency Scanning
# Run.github/workflows/security.yml
all- securityname: testsAudit cddependencies
src/drop-apprun: &&npm npx vitest runaudit --reporter verboseaudit-level=high
# RunHIGH specificor securityCRITICAL testCVEs filefail npxthe vitestbuild
run- src/__tests__/auth.test.tsname: Check for secrets in code
uses: trufflesecurity/trufflehog@main
# RunScans withfor coveragecommitted npxcredentials, vitestAPI run --coveragekeys
Blocking criteria: All security tests MUST pass before merge to main branch.
4. Testing Types, Tools & Schedule
4.1 SCA — Software Composition Analysis
Status: Implemented (npm audit)
| |
Current dependency security status (2026-02-13 audit):
| |||
| |||
| |||
| |||
| |||
|
Source: ~/ALAI/products/Drop/security/drop-security-rapport.md
Dependency update policy:
SecurityCRITICALpatchesCVE:(Critical/High): Mergepatch withinSLA24(see §5)hoursMinorHIGHsecurityCVE:updates: Mergepatch within147 daysMajorMEDIUMupdates:CVE:Planned migrationpatch within9030 days
ApprovedCVE: licenses:patch MIT,at Apachenext 2.0,sprint BSD (2-clause, 3-clause), ISC
4.2 SAST — Static Application Security Testing
Status: Planned (Phase 2)
SAST rules in scope (when implemented):
SQL injection detection (parameterized queries validation)JWT algorithm confusion (alg: nonedetection)Hardcoded secrets / credentialsInsecure cryptography (MD5, SHA-1, DES, RC4)Path traversal / LFIIDOR patterns (database queries without user_id scoping)Missing authentication middlewareboundary
4.3 DAST — Dynamic Application Security Testing (DAST)
OWASP ZAP
Status:Schedule: PlannedMonthly (Phase 3 —+ before productionevery launch)major release
Scope ( | in-scope |
https://staging. (staging environment only — NEVER | under |
DASTOut scanof scope:
AllSEF24portal, FINA portal (external systems)- Railway infrastructure
- Cloudflare WAF (managed by Cloudflare)
ZAP Configuration:
# zap-baseline.yaml
env:
contexts:
- name: Bilko API
endpointsurls:
- https://staging.bilko.io/api/
authentication:
method: script
# ZAP script to authenticate and get JWT
rules:
- id: 10202 # Absence of Anti-CSRF tokens — note (authenticatedcookies +are unauthenticated)httpOnly)
threshold: LOW
- id: 10096 # Timestamp Disclosure — ignore (timestamps are public)
threshold: OFF
Required ZAP findings threshold (before release):
- CRITICAL / HIGH: 0 allowed
AuthenticationMEDIUM:flowsmust(login,beregistration,assessedlogout,—sessionknownhandling)acceptable risks documentedRateLOWlimiting/(verifyINFORMATIONAL:10/60sdocumentlimitsandenforced)CSRF protection (Origin header validation)Input validation (IBAN, currency, language, amount)Security headers presence (HSTS, CSP, X-Frame-Options, etc.)SSL/TLS configuration (TLS 1.3, no TLS 1.0/1.1)JWT handling (expiry, algorithm, revocation)Feature flag enforcement (cards endpoints return 404 when disabled)prioritize
4.45. Penetration Testing
Status:Frequency: Required before Phase 3 production launchAnnual (Finanstilsynetor licensingafter requirement)
| External | |
Penetration test scope:Scope:
In-scope:
-- Web
Staging pre-launch: https://staging.getdrop.no
- Productionapplication (afterapp.bilko.io)
launch):- API
https://getdrop.no,endpoints
https://api.getdrop.no
- - Authentication
flows& session management
- Multi-tenant isolation (
login,primary registration, BankID OIDC callbackfocus — Phaseorg 2)isolation -must Allbe 24tested)
authenticated- Business
APIlogic endpoints
- Session managementflaws (JWT,VAT cookiecalculation, handling,invoice revocation)numbering)
-- Third-party
Rateintegrations limiting(SEF andAPI, CSRFHR-FISK)
protection
- AWS App Runner configuration
- Cloudflare WAF configuration
Out-of-scope:
- DenialRules of serviceengagement:
attacks
-- Staging
Socialenvironment only — no production testing without explicit CEO approval
- No DoS / DDoS testing
- No social engineering of
staffemployees
without- Penetration
priortest approvalagreement - AWS infrastructure itself (AWS global responsibility)
- BankID Norge AS systems
- Sumsub systems
Rules of engagement:
- Testing window: To be agreed in writing with provider
- Notifysigned before testing:engagement [email protected]begins
- Halt on critical finding: Immediately notify [email protected] + CTO
Key test areas for Drop:
JWT manipulation (algorithm confusion, expiry bypass, forged tokens)Session revocation bypass (sessions table manipulation)IDOR attacks (user_id scoping validation across all 24 endpoints)Rate limiting bypass (IP spoofing via X-Forwarded-For)CSRF (Origin header bypass)SQLi in all 24 endpoints (parameterized query verification)BankID OIDC callback manipulation (Phase 2)Feature flag bypass (cards endpoints when disabled)Foedselsnummer exposure (ensure never in plaintext logs or API responses)
5. Vulnerability Classification & Remediation SLAs
5.1 Severity Classification (CVSSpost-pentest v3.1)
findings):
| Severity | ||
|---|---|---|
5.2 Remediation SLAs
SLA tracking: GitHub Issues with label security-finding + severity label.
6. Security Code Review Checklist
RequiredWhen forrequired every(mandatory PRpre-merge):
- Changes
authorization,topayment processing, PII,authentication orcryptography:authorization code - Changes to encryption utilities (
encryptField,decryptField) - Changes to Prisma query patterns (potential org isolation bypass)
- New external API integrations (SEF, FINA, etc.)
- Changes to RBAC middleware or permission matrices
Who reviews: CTO or designated Senior Engineer with security background.
AuthenticationChecklist &for Authorization:security-sensitive PRs:
- No
hardcoded credentials, JWT secrets,secrets orAPIcredentialskeysin - code
Passwordorhashing uses bcrypt (cost >= 12) — SHA-256 explicitly rejected (fix C4) JWT validation: signature, expiry,iatclaim verified viajoseSession revocation: all protected endpoints check sessions table forrevoked = 0No role flags from user-supplied inputconfig- All
protectednew Prisma queries includeorganizationIdin WHERE clause - New endpoints have
authenticationRBACmiddlewaredecorator applied -
Logout revokes sessions server-side (not just clears cookie)
Input Validation:
AllNew user inputs validatedusingwithDropZodvalidators (validateAmount, validateIBAN, validateCurrency, validateLanguage, sanitizeText)schema-
AllL4databaseRestrictedoperationsfieldsuseencryptedparameterizedbeforewrite,?queriesdecrypted—afterno string concatenationread -
UserLoggedActiondataentryqueriescreatedincludeforANDalluser_idwrite= ?scoping (IDOR prevention)operations -
NoRateeval(),limitingexec(), or shell command interpolation with user input
Cryptography:
No MD5, SHA-1, DES, RC4 (per data-encryption-policy.md §2.2)Random values usecrypto.randomBytes()— not Math.random()IVs/nonces are random (96-bit minimum) and never reusedNo cryptographic keys in source code, logs, or error messages
Error Handling:
No stack traces or internal paths in API error responsesGeneric messages for authentication failures (no user enumeration)Errors loggedapplied toSentrynewinternallyauth-adjacentbut not exposed in API responses
Data Handling:
Foedselsnummer not logged in plaintext (never in Sentry, BetterStack, console.log)Sensitive data not in query parameters (use POST body)Feature flags checked before any cards-related endpointAML retention: user data deletion respects 5-year retention (Hvitvaskingsloven § 30)
Dependencies:
New dependencies reviewed withnpm auditbefore addingNo end-of-life packages introducedLicense compatible (MIT, Apache 2.0, BSD, ISC)endpoints
7. Security Testing in CI/CD PipelineSecurity Gates
Current pipeline (MVP):
Developer Commit
+-- Pre-commit hook: npm audit (SCA)
|
v
Git Push -> PR
|
v
Vitest security tests (20+ tests)
|
v (all pass)
Merge to main
|
v
Deploy to AWS App Runner
Target pipeline (Phase 2+):
flowchart LR
COMMIT[Developer\nCommit]PR["Pull Request"] --> LINT["ESLint Security\nRules"]
LINT -->|Pre-commit|"PASS"| SCA_INC[AUDIT["npm audit\nSCAn--audit-level=high"]
check]
SCA_INCAUDIT -->|Pass|"PASS"| PUSH[Git Push]
PUSH --> PR[Pull Request]
PR --> VITEST[TEST["Vitest\nSecurity Tests]Test PRSuite"]
--> SAST[SAST\nSemgrep/CodeQL]
PR --> SCA_FULL[npm audit\nFull scan]
PR --> SECRET[Secret\nScanning]
VITEST & SAST & SCA_FULL & SECRETTEST -->|All"PASS"| pass|SECRETS["TruffleHog\nSecret BUILD[Build\nDropScan"]
App]
BUILD --> STAGING[Deploy to\nStaging]
STAGING --> DAST[OWASP ZAP\nDAST Scan]
DASTSECRETS -->|Pass|"PASS"| GATE{Security\nGate}MERGE["Merge GATEAllowed"]
LINT -->|Pass|"FAIL"| PROD[DeployBLOCK["PR to\nProduction]Blocked"]
GATEAUDIT -->|Fail|"FAIL"| BLOCK[BlockBLOCK
Deploy\nAlertTEST [email protected]]-->|"FAIL"| BLOCK
SECRETS -->|"FAIL"| BLOCK
PipelineNon-negotiable gates (cannot be bypassed with --no-verify or --force):
- ESLint security
gaterules:criteria:zeroerrorfindingsGateToolBlocking Criteria CVEsPre-commit audit:- npm
audit HIGH/CRITICALCriticalzeroCVE typePR gate 1- Vitest security tests: 100% pass — especially org isolation and RBAC tests
zeroAny- TypeScript
teststrict:failureerrors PR gate 2SAST (Phase 2)Critical or High findingPR gate 3npm audit fullCritical CVEPR gate 4Secret scanningAny detected secretPost-stagingDAST (Phase 3)Critical or High dynamic finding - npm
8. SecurityVulnerability Audit History
Source: ~/ALAI/products/Drop/security/drop-security-rapport.md
Security Hardening Summary (2026-02-13)
| ||
| ||
Remaining items:
| |||
|
9. Reporting Format
9.1 Individual Finding
Finding ID: VULN-{YEAR}-{SEQUENCE}
Title: {SHORT_DESCRIPTION}
Severity: Critical / High / Medium / Low
CVSS Score: {SCORE} (v3.1)
Description:
{DETAILED_DESCRIPTION}
Affected Endpoint / File:
- {ENDPOINT_OR_FILE}: {URL_OR_LINE_NUMBER}
Proof of Concept:
{STEPS_TO_REPRODUCE}
Impact:
{WHAT_AN_ATTACKER_CAN_DO}
GDPR/AML Implications:
{IF_PERSONAL_DATA_OR_FINANCIAL_DATA_AFFECTED}
Remediation:
{SPECIFIC_FIX}
Owner: {ASSIGNED_ENGINEER}
SLA Due Date: {DATE}
Status: Open / In Progress / Resolved / Accepted Risk
10. Metrics & KPIs
11. Bug Bounty ProgramDisclosure
Status:Process: Planned (Phase 3 — post-launch)
Platform: Intigriti or HackerOne
Responsible
disclosure(interimresearchers —may before bug bounty):
Report securityreport vulnerabilities to [email protected].
Approval
| Role | Name | |||
|---|---|---|---|---|
| Author | CTO / Security |
2026-02-23 | ||
| CEO |