<< All versions
Skill v1.0.0
currentAutomated scan100/100navikt/copilot/security-review
──Details
PublishedApril 30, 2026 at 12:41 AM
Content Hashsha256:d6a6bbddfc7f18c0...
Git SHA4b477bf35a7f
──Files
Files (1 file, 7.2 KB)
SKILL.md7.2 KBactive
SKILL.md · 256 lines · 7.2 KB
version: "1.0.0" name: security-review description: Bruk før commit, push eller pull request for å sjekke at koden er trygg å merge license: MIT metadata: domain: auth tags: security pre-commit vulnerability-scanning code-review
Security Review Skill
This skill provides pre-commit and pre-PR security checks for Nav applications. Covers secret scanning, vulnerability scanning, and Nav-specific requirements.
For architecture questions, threat modeling, or compliance decisions, use @security-champion instead.
Automated Scans
Run with run_in_terminal:
bash
# Scan repo for known vulnerabilities and secretstrivy repo .# Scan Docker image for HIGH/CRITICAL CVEstrivy image <image-name> --severity HIGH,CRITICAL# Scan GitHub Actions workflows for insecure patternszizmor .github/workflows/# Quick search for secrets in git historygit log -p --all -S 'password' -- '*.kt' '*.ts' | head -100git log -p --all -S 'secret' -- '*.kt' '*.ts' | head -100
Parameterized SQL (Never Concatenate)
kotlin
// ✅ Correct – parameterized queryfun findBruker(fnr: String): Bruker? =jdbcTemplate.queryForObject("SELECT * FROM bruker WHERE fnr = ?",brukerRowMapper,fnr)// ❌ Wrong – SQL injection riskfun findBrukerUnsafe(fnr: String): Bruker? =jdbcTemplate.queryForObject("SELECT * FROM bruker WHERE fnr = '$fnr'",brukerRowMapper)
No PII in Logs
kotlin
// ✅ Correct – log correlation ID, not PIIlog.info("Behandler sak for bruker", kv("sakId", sak.id), kv("tema", sak.tema))// ❌ Wrong – never log FNR, name, or other PIIlog.info("Behandler sak for bruker ${bruker.fnr}") // GDPR violationlog.info("Navn: ${bruker.navn}") // GDPR violation
Secrets from Environment, Never Hardcoded
kotlin
// ✅ Correct – read from environment (Nais injects via Secret)val dbPassword = System.getenv("DB_PASSWORD")?: throw IllegalStateException("DB_PASSWORD mangler")// ❌ Wrong – hardcoded secretval dbPassword = "supersecret123"
Network Policy (Nais)
Only expose what must be exposed:
yaml
spec:accessPolicy:inbound:rules:- application: frontend-app # only explicitly named callersoutbound:rules:- application: pdl-apinamespace: pdlcluster: prod-gcpexternal:- host: api.external-service.no # only if strictly necessary
OWASP Top 10 Checks
A01: Broken Access Control
kotlin
// ✅ Correct — check that user has access to the resource@GetMapping("/api/vedtak/{id}")fun getVedtak(@PathVariable id: UUID): ResponseEntity<VedtakDTO> {val bruker = hentInnloggetBruker()val vedtak = vedtakService.findById(id)if (vedtak.brukerId != bruker.id) {return ResponseEntity.status(HttpStatus.FORBIDDEN).build()}return ResponseEntity.ok(vedtak.toDTO())}// ❌ Wrong — no access control (IDOR)@GetMapping("/api/vedtak/{id}")fun getVedtak(@PathVariable id: UUID) = vedtakService.findById(id)
A03: Injection
kotlin
// ✅ Correct — parameterized queryjdbcTemplate.query("SELECT * FROM bruker WHERE fnr = ?", mapper, fnr)// ❌ Wrong — string concatenationjdbcTemplate.query("SELECT * FROM bruker WHERE fnr = '$fnr'", mapper)
A05: Security Misconfiguration
kotlin
// ✅ Correct — CORS only for known domains@Beanfun corsFilter() = CorsFilter(CorsConfiguration().apply {allowedOrigins = listOf("https://my-app.intern.nav.no")allowedMethods = listOf("GET", "POST")allowedHeaders = listOf("Authorization", "Content-Type")})// ❌ Wrong — open CORSallowedOrigins = listOf("*")
A07: Cross-Site Scripting (XSS)
tsx
// ✅ Correct — React escapes automatically<BodyShort>{bruker.navn}</BodyShort>// ❌ Wrong — raw HTML injection<div dangerouslySetInnerHTML={{ __html: userInput }} />
A08: Insecure Deserialization
kotlin
// ✅ Correct — validate input after deserialization@PostMapping("/api/vedtak")fun create(@RequestBody @Valid request: CreateVedtakRequest): ResponseEntity<VedtakDTO>// ✅ Limit Jackson to known typesobjectMapper.apply {activateDefaultTyping(polymorphicTypeValidator,ObjectMapper.DefaultTyping.NON_FINAL)}
A09: Logging & Monitoring
kotlin
// ✅ Correct — structured logging with correlation ID, no PIIlog.info("Vedtak opprettet", kv("vedtakId", vedtak.id), kv("sakId", sak.id))// ❌ Wrong — PII in logslog.info("Vedtak for bruker ${bruker.fnr} opprettet")
File Upload Security
kotlin
// ✅ Correct — validate file type, size, and magic bytesfun validateUpload(file: MultipartFile) {require(file.size <= 10 * 1024 * 1024) { "File too large (max 10 MB)" }require(file.contentType in ALLOWED_TYPES) { "Invalid file type" }val bytes = file.bytes.take(8).toByteArray()require(verifyMagicBytes(bytes, file.contentType!!)) { "File content does not match type" }}private val ALLOWED_TYPES = setOf("application/pdf", "image/png", "image/jpeg")
Dependency Management
kotlin
// build.gradle.kts — pin versions, use BOMdependencyManagement {imports {mavenBom("org.springframework.boot:spring-boot-dependencies:3.4.1")}}// Check vulnerable dependencies// ./gradlew dependencyCheckAnalyze// trivy repo .
Expanded Checklist
- [ ] SQL queries are parameterized (no string concatenation)
- [ ] No PII in logs (fnr, name, address)
- [ ] Secrets only from environment/secrets
- [ ] Nais accessPolicy is explicit (no open inbound)
- [ ] CORS is restricted to known domains
- [ ] Input is validated and sanitized
- [ ] Access control checks ownership (not just auth)
- [ ] File upload validates type, size, and content
- [ ] Dependencies are up to date and vulnerability-scanned
- [ ] No
dangerouslySetInnerHTMLwithout sanitization
Dependency Management
bash
# Kotlin – check for outdated/vulnerable dependencies./gradlew dependencyUpdates./gradlew dependencyCheckAnalyze # OWASP check# Node/TypeScriptnpm auditnpm audit fix
Security Checklist
- [ ] No secrets, tokens, or API keys hardcoded in source
- [ ] No PII (FNR, name, address) in log statements
- [ ] All SQL queries use parameterized statements
- [ ] Nais
accessPolicylimits inbound/outbound to only what is needed - [ ] Token validation on all protected endpoints (see
@security-champion) - [ ] M2M tokens validate
azpagainstAZURE_APP_PRE_AUTHORIZED_APPS - [ ] Auth code matches
.nais/accessPolicy inbound rules (no dead code or missing rules) - [ ]
trivy repo .passes without HIGH/CRITICAL findings - [ ]
zizmorpasses on all GitHub Actions workflows - [ ] Git history clean of committed secrets (
git logscan above) - [ ] HTTPS enforced – no plain HTTP calls to external services
- [ ] Dependencies up to date (
dependencyUpdates/npm audit)
Related
| Resource | Use For | |
|---|---|---|
@security-champion | Threat modeling, compliance questions, Nav security architecture | |
@auth-agent | JWT validation, TokenX, ID-porten, Maskinporten | |
@nais-agent | Nais manifest, accessPolicy, secrets setup | |
| sikkerhet.nav.no | Nav Golden Path, authoritative security guidance |