Security Testing Policy
Security Testing Policy
UpdatedProject2026-04-29/for ADR-021 path realignment.Seedocs/architecture/ADR-021-bilko-blueprint-section-15-realignment.md.
Organization:
BilkoALAI Holding AS —BalkanDropAccountingPaymentSaaSApp Policy Number: POL-SEC-TEST-001 Version: 1.0 Date: 2026-02-23 Author:CTO /SecurityEngineerArchitect Status: Draft Reviewers: CISO, Engineering Lead,DPOCTO Next Review: 2027-02-23 Classification: Confidential
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-23 | Initial draft — Drop security testing |
1. Purpose & Scope
Purpose: This policy defines the security testing requirements,methodology, tools, schedule,frequency, and acceptanceremediation criteriarequirements for all systems operated by ALAI Holding AS for the BilkoDrop platform.payment Bilko handles regulated financial data (tax IDs, IBAN, accounting records) across three jurisdictions.app. Security testing is mandatory,mandatory not— optional.no system goes to production without completing applicable security tests.
Regulatory basis:
- IKT-forskriften (FOR-2003-05-21-630) §§ 5-6 — ICT security verification
- DORA (EU) 2022/2554 Art. 24-25 — Digital operational resilience testing
- Finanstilsynet licensing requirements — penetration test before production launch
- GDPR Art. 32(1)(d) — regular testing of technical measures
Scope:
- All
BilkoproductionapplicationsAPIs—andExpress APIendpoints (,apps/api-express/getdrop.noapi.getdrop.no), - All
frontendAWS infrastructure (apps/web/),AppdatabaseRunner,layerS3,(PrismaKMS,+SecretsPostgreSQL),Manager) - All CI/CD pipelines and
externalbuild systems - All third-party integrations (
SEF,BankID,HR-FISK).Sumsub,TheOpenKotlin/KtorBankingbackendpartners) - Developer workstations handling Confidential or Restricted data
Policy Owner: CISO (apps/api/)[email protected])
isOperational coveredOwner: separatelySecurity asteam it+ maturesEngineering (MC #5125).Lead
2. Security Testing PyramidMethodology
Approach: Shift-left security — testing integrated throughout development, not bolted on at the end.
Testing layers:
graphDevelopment TDPhase
subgraph+-- AUTOMATED["AutomatedUnit security tests — Vitest (runscurrent: 20+ security-specific tests)
+-- SCA — npm audit (every commit, automated)
+-- Secret scanning — detect leaked credentials (every commit)
Build / CI pipeline)"]
SAST["SAST\nESLint Security Rules\nTypeScript strict mode\nnpm audit\nSnyk SCA"]
UNIT["Security Unit Tests\nVitest\nRBAC matrix tests\nOrg isolation tests\nEncryption tests\nVAT accuracy tests"]
INT["Integration Tests\nVitestPhase
+-- Supertest\nAuthSAST flow— tests\nJWTstatic validation\nRatecode limiting tests"]
end
subgraph PERIODIC["Periodicanalysis (scheduled)"]every DAST["DAST\nOWASPPR ZAP\nMonthly— planned Phase 2)
+-- pre-release"]SCA E2E["Security— E2E\nPlaywright\nCross-tenantdependency boundaryvulnerability tests\nPrivilege escalation tests"]
end
subgraph MANUAL["Manualcheck (scheduled)"]every PENTEST["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\nExternaltest vendor\nAnnual"]— REVIEW["Securitymanual Codeexpert Review\nPre-mergetesting (security-sensitivebefore PRs)\nArchitecturePhase 3 launch)
+-- Post-incident review quarterly"]— endregression UNITtests -->after INTany -->security DAST --> PENTESTincident
3. AutomatedCurrent Security TestingTest (CI/CD)Coverage
Every push to main and every pull request triggers:
3.1 StaticVitest AnalysisUnit (SAST)Security Tests
Status: Implemented — 20+ security-specific tests
Source: src/drop-app/src/__tests__/
Authentication |
|
|
|
auth.test.ts |
|
Authentication |
auth.test.ts |
|
auth.test.ts |
||
| Authentication | Logout revokes all user sessions | auth.test.ts |
| Password | bcrypt used — not SHA-256 | utils-server.test.ts |
| Password | bcrypt cost factor >= 12 | utils-server.test.ts |
| Password | SHA-256 hash rejected as invalid (fix C4) | utils-server.test.ts |
| Input Validation | IBAN checksum validation | validation.test.ts |
| Input Validation | Currency whitelist enforced | validation.test.ts |
| Input Validation | Language whitelist enforced | validation.test.ts |
| Input Validation | Amount: NaN/Infinity rejected | validation.test.ts |
| Input Validation | HTML tags stripped by sanitizeText() | validation.test.ts |
| SQL Injection | Parameterized queries for all user inputs | db.test.ts |
| Rate Limiting | Auth rate limit: 10/60s enforced | middleware.test.ts |
| Rate Limiting | Transaction rate limit: 10/60s enforced | middleware.test.ts |
| CSRF | Origin header validated | middleware.test.ts |
| CSRF | Invalid origin rejected | middleware.test.ts |
| IDOR | Transaction query scoped to user_id | transactions.test.ts |
| IDOR | Recipient query scoped to user_id | recipients.test.ts |
| Feature Flags | Cards endpoint returns 404 when flag disabled | feature-flags.test.ts |
3.2 Running the Security Test Suite
# Run all security tests
cd src/drop-app && npx vitest run --reporter verbose
# Run specific security test file
npx vitest run src/__tests__/auth.test.ts
# Run with coverage
npx vitest run --coverage
Blocking criteria: All security tests MUST pass before merge to main branch.
4. Testing Types, Tools & Schedule
4.1 SCA including— licenseSoftware complianceComposition Analysis
Status: Implemented (npm audit)
| Property | Value |
|---|---|
| Tool | npm audit (built-in) + GitHub Dependabot (planned) |
| Frequency | Every commit (pre-commit hook) + every PR |
| Blocking | YES — Critical CVE blocks merge |
3.2
Current Securitydependency Unitsecurity Testsstatus (Vitest)2026-02-13
Location: audit):apps/api-express/src/__tests__/security/
Source: Dependency update policy: Approved licenses: MIT, Apache 2.0, BSD (2-clause, 3-clause), ISC Status: Planned (Phase 2) SAST rules in scope (when implemented): Penetration test scope: Remediation SLAs
SLA tracking: GitHub Issues with label Required for every PR touching authentication, authorization, payment processing, PII, or cryptography: Authentication & Authorization: Input Validation: Cryptography: Error Handling: Data Handling: Dependencies: Current pipeline (MVP): Target pipeline (Phase 2+): Pipeline security gate criteria: Source: Remaining items: Responsible disclosure (interim — before bug bounty):
Report security vulnerabilities to [email protected].Required
test
suites:
RBACPackage
MatrixVersion
TestsRisk
Status
//joseEvery^6.1.3
permissionLow
combinationNo
mustknown beCVEsexplicitlytested
describe('RBACbcryptjs—^3.0.3
InvoiceLow
access',No
()known =>CVEs{const
rolesbetter-sqlite3=^12.6.2
['owner',Low
'admin',No
'accountant',known 'viewer']CVEstest.each([['owner',
'create',nexttrue],16.1.6
['admin',Low
'create',Recent
true],version['accountant','create',
true],react['viewer',19.2.3
'create',Low
false],Latest
['owner',major'delete',true],
['admin',radix-ui'delete',^1.4.3
true],Low
['accountant',UI
'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 isolationonly — 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
# .github/workflows/security.yml
- name: Audit dependencies
run: npm audit --audit-level=high
# HIGH or CRITICALknown CVEsfailthe
build-name: Check for secrets in code
uses: trufflesecurity/trufflehog@main
# Scans for committed credentials, API keys
~/ALAI/products/Drop/security/drop-security-rapport.md
CRITICALSecurity CVE:patches patch(Critical/High): Merge within 24SLA hours(see §5)HIGHMinor CVE:security patchupdates: Merge within 714 daysMEDIUMMajor CVE:updates: patchPlanned migration within 3090 days
4.2 SAST — Static Application Security Testing
Property
Value
Tool
Semgrep or CodeQL (via GitHub Advanced Security)
Frequency
Every PR (full scan)
Languages
TypeScript, JavaScript
Blocking
YES — Critical or High findings block merge
LOWSQL CVE:injection patchdetection at(parameterized nextqueries sprintvalidation)boundaryalg: none detection)
4.3 DAST — Dynamic Application Security Testing
(DAST)
OWASP ZAPSchedule:Status: MonthlyPlanned (Phase 3 — before production launch)
Property
Value
Tool
OWASP ZAP (planned)
Frequency
Every deployment to staging +
beforeweekly everyfull majorscanreleaseScope
(in-scopeTarget
for ZAP):
https://staging.bilko.iogetdrop.no (staging environment only — NEVER production)production Allwithout APICISO endpointswritten underapproval/api/Authentication
flowsBlocking
FileYES
upload— endpointsdeployment (halted if any)Critical finding discoveredOutDAST ofscan scope:
SEFAll portal,24 FINAAPI portalendpoints (externalauthenticated systems)+ unauthenticated)RailwayAuthentication infrastructureflows (login, registration, logout, session handling)CloudflareRate WAFlimiting (managedverify by10/60s Cloudflare)limits enforced)
4.4 Penetration Testing
ZAPStatus: Configuration:Required before Phase 3 production launch (Finanstilsynet licensing requirement)
Property
Value
Frequency
Before production launch + annual thereafter
Scope
Full application + API + AWS infrastructure
Methodology
OWASP Testing Guide v4.2 + PTES
Provider
External security firm (not yet selected)
Blocking
Critical findings: system taken offline until remediated
# zap-baseline.yaml
env:
contexts:In-scope:
- name:Staging Bilko API
urls:
-pre-launch: https://staging.bilko.io/api/getdrop.no
authentication:- method:Production script(after #launch): ZAPhttps://getdrop.no, scripthttps://api.getdrop.no
to- authenticateAuthentication flows (login, registration, BankID OIDC callback — Phase 2)
- All 24 authenticated API endpoints
- Session management (JWT, cookie handling, revocation)
- Rate limiting and getCSRF JWT
rules:protection
- id:AWS 10202App #Runner Absenceconfiguration
- Cloudflare WAF configuration
Out-of-scope:
- Denial of Anti-CSRFservice tokens — note (cookies are httpOnly)
threshold: LOWattacks
- id:Social 10096engineering #of Timestampstaff Disclosurewithout —prior ignoreapproval
- AWS infrastructure itself (timestampsAWS areglobal public)responsibility)
threshold:- OFFBankID Norge AS systems
- Sumsub systems
Rules of engagement:
- Testing window: To be agreed in writing with provider
- Notify before testing: [email protected]
- Halt on critical finding: Immediately notify [email protected] + CTO
RequiredKey ZAPtest findingsareas thresholdfor (before release):Drop:
CRITICALJWT /manipulation HIGH:(algorithm 0confusion, allowedexpiry bypass, forged tokens)MEDIUM:Session mustrevocation bebypass assessed(sessions —table known acceptable risks documentedmanipulation)LOWIDOR /attacks INFORMATIONAL:(user_id documentscoping andvalidation prioritizeacross all 24 endpoints)
5.
PenetrationVulnerability Testing
Frequency: Annual (or after significant architecture change)
Provider: External certified pentest firm (OSCP/CREST certified)
Scope:
Web application (app.bilko.io)
API endpoints
AuthenticationClassification & session management
Multi-tenant isolation (primary focus — org isolation must be tested)
Business logic flaws (VAT calculation, invoice numbering)
Third-party integrations (SEF API, HR-FISK)
Rules of engagement:
Staging environment only — no production testing without explicit CEO approval
No DoS / DDoS testing
No social engineering of employees
Penetration test agreement signed before engagement begins
5.1 Severity Classification (
post-pentestCVSS findings):
Severity
FixCVSS DeadlineScoreDrop-Specific Definition
CRITICALCritical489.0-10.0hoursRCE, auth bypass without credentials, mass PII exfiltration, JWT forgery, foedselsnummer exposure
HIGHHigh77.0-8.9daysPrivilege escalation, IDOR (cross-user data access), auth bypass (authenticated), SQL injection
MEDIUMMedium304.0-6.9daysCSRF, reflected XSS, rate limit bypass, information disclosure
LOWLow0.1-3.9
Version disclosure, weak error messages, minor misconfigurations
Informational
N/A
Best practice recommendations
5.2 Remediation SLAs
Severity
SLA
Action on Breach of SLA
Critical
Containment in 4h; full remediation in 24 hours
Emergency — CISO + CTO + CEO. System taken offline if risk cannot be mitigated.
High
7 calendar days
Escalate to CISO. Engineering Lead must approve extension.
Medium
30 calendar days
Engineering Lead tracks. Exception requires Security team sign-off.
Low
90 calendar days
Tracked in backlog. Reviewed in quarterly security review.
Informational
Next
quartersprint (best effort)Not SLA-bound.
security-finding + severity label.
6. Security Code Review Checklist
iat claim verified via joserevoked = 0
? queries — no string concatenationAND user_id = ? scoping (IDOR prevention)
crypto.randomBytes() — not Math.random()
npm audit before adding
7. Security Testing in CI/CD Pipeline
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
flowchart LR
COMMIT[Developer\nCommit] -->|Pre-commit| SCA_INC[npm audit\nSCA check]
SCA_INC -->|Pass| PUSH[Git Push]
PUSH --> PR[Pull Request]
PR --> VITEST[Vitest\nSecurity Tests]
PR --> SAST[SAST\nSemgrep/CodeQL]
PR --> SCA_FULL[npm audit\nFull scan]
PR --> SECRET[Secret\nScanning]
VITEST & SAST & SCA_FULL & SECRET -->|All pass| BUILD[Build\nDrop App]
BUILD --> STAGING[Deploy to\nStaging]
STAGING --> DAST[OWASP ZAP\nDAST Scan]
DAST -->|Pass| GATE{Security\nGate}
GATE -->|Pass| PROD[Deploy to\nProduction]
GATE -->|Fail| BLOCK[Block Deploy\nAlert [email protected]]
Gate
Tool
Blocking Criteria
Pre-commit
npm audit
Critical CVE
PR gate 1
Vitest security tests
Any test failure
PR gate 2
SAST (Phase 2)
Critical or High finding
PR gate 3
npm audit full
Critical CVE
PR gate 4
Secret scanning
Any detected secret
Post-staging
DAST (Phase 3)
Critical or High dynamic finding
6.8. Security CodeAudit ReviewHistory
Date
Type
Provider
Findings
Status
2026-02-12
Full security audit
Internal (Security Architect)
4C / 5H / 6M / 4L
Complete
2026-02-13
Hardening verification
Internal
0C / 0H / 2M / 4L
Complete
TBD (Phase 3)
External penetration test
TBD
—
Planned — required before launch
TBD (Phase 3+)
Annual pentest
TBD
—
Annual thereafter
~/ALAI/products/Drop/security/drop-security-rapport.mdSecurity Hardening Summary (2026-02-13)
Finding
ID
Status
Card data: only
last_four + token_ref stored (no PAN/CVV)C1
Resolved
Demo credentials gated behind
NODE_ENV !== 'production'C2
Resolved
SHA-256 password support removed — bcrypt only
C4
Resolved
Session revocation implemented and active
C6/H1
Resolved
Input sanitization applied to all text fields
H4
Resolved
Notification IDs validated (max 100, format check)
M5
Resolved
Settings: currency/language validated against whitelists
M6
Resolved
Finding
ID
Type
Plan
CSP:
unsafe-inline/unsafe-eval required by Next.jsM1
Medium
Nonce-based CSP in Phase 3
Proxy
HOSTNAME configM2
Medium
Resolve in Phase 2 AWS config
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
Metric
Target
Reporting Frequency
Critical findings resolved within SLA
100%
Monthly
High findings resolved within SLA
100%
Monthly
Vitest security test pass rate
100% (blocking)
Every PR
MTTR — Critical
< 24 hours
Per incident
MTTR — High
< 7 days
Monthly
Open High+ findings
0 at launch
Monthly
Annual penetration test completed
Yes (before Phase 3)
Annual
npm audit Critical CVEs
0 in production
Continuous
11. Bug Bounty Program
WhenStatus: requiredPlanned (mandatoryPhase pre-merge)3 — post-launch)
Platform: Intigriti or HackerOne
ChangesAcknowledgment: towithin authentication1 orbusiness authorization codedayChanges 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.
Checklist for security-sensitive PRs:
No secrets or credentials in code or config
All new Prisma queries include organizationId in WHERE clause
New endpoints have RBAC decorator applied
New user inputs validated with Zod schema
L4 Restricted fields encrypted before write, decrypted after read
LoggedAction entry created for all write operations
Rate limiting applied to new auth-adjacent endpoints
7. CI/CD Security Gates
flowchart LR
PR["Pull Request"] --> LINT["ESLint Security\nRules"]
LINT -->|"PASS"| AUDIT["npm audit\n--audit-level=high"]
AUDIT -->|"PASS"| TEST["Vitest\nSecurity Test Suite"]
TEST -->|"PASS"| SECRETS["TruffleHog\nSecret Scan"]
SECRETS -->|"PASS"| MERGE["Merge Allowed"]
LINT -->|"FAIL"| BLOCK["PR Blocked"]
AUDIT -->|"FAIL"| BLOCK
TEST -->|"FAIL"| BLOCK
SECRETS -->|"FAIL"| BLOCK
Non-negotiable gates (cannot be bypassed with --no-verify or --force):
ESLint security rules: zero error findings
npm audit: zero HIGH/CRITICAL CVEs
Vitest security tests: 100% pass — especially org isolation and RBAC tests
TypeScript strict: zero type errors
8. Vulnerability Disclosure
Process:
Security researchers may report vulnerabilities to [email protected]
Acknowledgment within 24 hours
Investigation and severity assessmentTriage: within 5 business daysRemediationSafe perharbor: SLAresearchers acting in Sectiongood 5faith are protected from legal actionResponsible disclosure: researcher notified when fix is deployed
Approval
Role
Name
SignatureDateDateSignature
Author
CTO / Security Engineer
Architect
2026-02-23
Reviewer (Engineering Lead)CISO
ReviewerEngineering (DPO)Lead
ApproverCTOCEO
Management