1. 醫療保健安全測試概述

1.1。為什麼 Pentest 醫療保健系統與眾不同?
與傳統 IT 系統相比,醫療系統的安全測試需要特殊的方法。醫療數據在暗網上具有最高的價值——比信用卡高 10-50 倍——因為它包含不可變的信息,例如病史、遺傳密碼、社會保險號碼。

| 挑戰 | 詳情 |
|---|---|
| 監管合規性 | HIPAA §164、ISO 27799、法令 13/2023、風險評估 |
| 資料敏感度 | ePHI/PHI、遺傳、心理健康、藥物濫用 |
| 系統可用性 | 24/7 操作、生命攸關、無停機、ER 系統 |
限制: 不能擾亂患者護理,不能訪問/暴露真實的 PHI,必須維護審計跟踪,結果 = 法律證據。必須使用合成資料進行測試並獲得正式授權(BAA 修正案)。
1.2。醫療保健安全測試框架

5 個階段:
- 規劃與範圍界定 → 2. 發現與偵察 → 3. 攻擊模擬 → 4. 報告與證據 → 5. 修復與重新測試
HIPAA 映射:
- §164.308(a)(8) — 評估(必填)
- §164.312(a)(1) — 存取控制驗證
- §164.312(e)(1) — 傳輸安全測試
- §164.306(e) — 更新了風險分析
1.3。安全測試的分類
| 測驗類型 | 說明 | 工具 | 何時跑步 |
|---|---|---|---|
| 先科科技 | 靜態應用安全測試-原始碼分析 | SonarQube、Semgrep、SpotBugs | 每次提交/PR |
| 達斯特 | 動態應用程式安全測試——測試運行應用程式 | OWASP ZAP、Nuclei、Burp Suite | 每次發布 |
| IAST | 互動式 — 執行時期代理程式 | 對比安全 | 執行整合測試時 |
| SCA | 軟體構成分析-依賴性檢查 | Snyk、Dependabot、Trivy | 每次提交+預定 |
| 滲透測試 | 手動滲透測試 | 手冊+工具 | 季度/年度 |
| 配置審核 | 基礎設施配置回顧 | CIS-CAT、Lynis、kube-bench | 每月 |
2. Quarkus 的靜態應用程式安全測試 (SAST)
2.1。醫療保健 Java 的 SonarQube 配置
# sonar-project.properties
sonar.projectKey=healthcare-microservices
sonar.projectName=Healthcare Microservices Platform
sonar.organization=hospital-dev
# Source
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.java.binaries=target/classes
sonar.java.test.binaries=target/test-classes
# Quality Gate — Healthcare (stricter than default)
sonar.qualitygate=Healthcare-Critical
# Healthcare-specific rules
sonar.issue.ignore.multicriteria=e1
sonar.issue.ignore.multicriteria.e1.ruleKey=java:S106
sonar.issue.ignore.multicriteria.e1.resourceKey=**/HealthCheckResource.java
# Coverage requirements
sonar.coverage.jacoco.xmlReportPaths=target/jacoco-report/jacoco.xml
// Healthcare-specific SonarQube Custom Rules
// custom-rules/src/main/java/vn/xdev/sonar/healthcare/
/**
* Rule: PHI Data Logging Prevention
* Detects potential PHI leakage in log statements
*/
@Rule(
key = "HealthcarePHILogging",
name = "PHI Data Must Not Be Logged in Plaintext",
description = "Prevents logging of PHI fields such as patient name, SSN, " +
"diagnosis, or medical record number in plaintext",
priority = Priority.BLOCKER,
tags = {"hipaa", "phi", "security"}
)
public class PHILoggingCheck extends IssuableSubscriptionVisitor {
private static final Set<String> PHI_FIELD_PATTERNS = Set.of(
"patientName", "patientId", "ssn", "socialSecurityNumber",
"dateOfBirth", "dob", "diagnosis", "diagnosisCode",
"medicalRecordNumber", "mrn", "insuranceId",
"phoneNumber", "address", "email", "labResult",
"prescription", "treatmentPlan", "geneticData"
);
@Override
public List<Tree.Kind> nodesToVisit() {
return List.of(Tree.Kind.METHOD_INVOCATION);
}
@Override
public void visitNode(Tree tree) {
MethodInvocationTree methodInvocation = (MethodInvocationTree) tree;
if (isLoggingMethod(methodInvocation)) {
Arguments arguments = methodInvocation.arguments();
for (ExpressionTree arg : arguments) {
if (containsPHIField(arg)) {
reportIssue(arg,
"Potential PHI data detected in log statement. " +
"Use PHI masking or remove the field. " +
"HIPAA §164.312(a)(1) — Access Control.");
}
}
}
}
private boolean isLoggingMethod(MethodInvocationTree mit) {
String methodName = mit.methodSymbol().name();
return Set.of("info", "debug", "warn", "error", "trace", "log")
.contains(methodName);
}
private boolean containsPHIField(ExpressionTree expression) {
String exprText = expression.toString().toLowerCase();
return PHI_FIELD_PATTERNS.stream()
.anyMatch(field -> exprText.contains(field.toLowerCase()));
}
}
2.2。 Semgrep 醫療保健規則
# .semgrep/healthcare-rules.yaml
rules:
# Rule 1: Detect unhashed PHI in database queries
- id: healthcare-phi-in-query
patterns:
- pattern: |
$QUERY = "... " + $PHI_VAR + " ...";
- metavariable-regex:
metavariable: $PHI_VAR
regex: (?i)(patient|ssn|diagnosis|mrn|dob|insurance)
message: >-
Potential PHI concatenation in SQL query detected.
Use parameterized queries to prevent SQL injection and PHI exposure.
HIPAA §164.312(a)(1) violation risk.
severity: ERROR
languages: [java]
metadata:
category: security
subcategory: hipaa
confidence: HIGH
# Rule 2: Detect PHI in HTTP responses without encryption
- id: healthcare-phi-response-unencrypted
patterns:
- pattern: |
Response.ok($DATA)
- metavariable-regex:
metavariable: $DATA
regex: (?i).*(patient|diagnosis|labResult|prescription).*
- pattern-not-inside: |
@Encrypted
...
message: >-
PHI data returned in response without field-level encryption annotation.
Consider using @EncryptedResponse for PHI fields.
severity: WARNING
languages: [java]
# Rule 3: Detect hardcoded encryption keys
- id: healthcare-hardcoded-crypto-key
patterns:
- pattern-either:
- pattern: |
SecretKeySpec($KEY, ...)
- pattern: |
new SecretKey(...)
- pattern-not-inside: |
... = vault.getSecret(...)
...
message: >-
Encryption key appears to be hardcoded. Healthcare encryption keys
must be managed by a KMS (HashiCorp Vault).
HIPAA §164.312(a)(2)(iv) — Encryption and Decryption.
severity: ERROR
languages: [java]
# Rule 4: Detect missing audit logging on PHI access
- id: healthcare-phi-access-no-audit
patterns:
- pattern: |
public $RETURN $METHOD(...) {
...
$REPO.find$ENTITY(...);
...
}
- metavariable-regex:
metavariable: $ENTITY
regex: (?i)(Patient|Encounter|Observation|DiagnosticReport|MedicationRequest)
- pattern-not-inside: |
...
auditService.$LOG(...);
...
message: >-
PHI entity accessed without audit logging.
All FHIR resource access must be audited per HIPAA §164.312(b).
severity: ERROR
languages: [java]
2.3。 Quarkus 的 SpotBugs 安全插件
<!-- pom.xml — SpotBugs configuration -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.8.6.5</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<failOnError>true</failOnError>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.13.0</version>
</plugin>
</plugins>
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
</configuration>
<executions>
<execution>
<phase>verify</phase>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
3. 動態應用程式安全測試 (DAST)
3.1。用於醫療保健 API 的 OWASP ZAP
# zap-healthcare-scan.yaml — ZAP Automation Framework
---
env:
contexts:
- name: "Healthcare API"
urls:
- "https://api.hospital.local"
includePaths:
- "https://api.hospital.local/api/v1/.*"
excludePaths:
- "https://api.hospital.local/api/v1/health"
authentication:
method: "json"
parameters:
loginPageUrl: "https://keycloak.hospital.local/realms/hospital/protocol/openid-connect/token"
loginRequestUrl: "https://keycloak.hospital.local/realms/hospital/protocol/openid-connect/token"
loginRequestBody: >-
grant_type=client_credentials
&client_id=zap-scanner
&client_secret={%env:ZAP_CLIENT_SECRET%}
verification:
method: "response"
loggedInRegex: "\\Qaccess_token\\E"
technology:
include:
- "Java"
- "PostgreSQL"
- "Linux"
parameters:
failOnError: true
failOnWarning: false
progressToStdout: true
jobs:
# Step 1: OpenAPI Import
- type: openapi
parameters:
apiUrl: "https://api.hospital.local/q/openapi"
context: "Healthcare API"
# Step 2: Active Scan with healthcare-specific policies
- type: activeScan
parameters:
context: "Healthcare API"
policy: "Healthcare-HIPAA"
maxRuleDurationInMins: 5
maxScanDurationInMins: 60
policyDefinition:
rules:
# SQL Injection — critical for PHI databases
- id: 40018
name: "SQL Injection"
strength: INSANE
threshold: LOW
# XSS — patient portals are targets
- id: 40012
name: "Cross Site Scripting (Reflected)"
strength: HIGH
threshold: LOW
# IDOR — patient data isolation
- id: 40035
name: "Out of Band XSS"
strength: HIGH
threshold: MEDIUM
# Path Traversal — access to config files
- id: 6
name: "Path Traversal"
strength: HIGH
threshold: LOW
# SSRF — internal service access
- id: 40046
name: "Server Side Request Forgery"
strength: HIGH
threshold: LOW
# Step 3: Specific healthcare checks
- type: requestor
parameters:
# Test IDOR on patient endpoints
requests:
- url: "https://api.hospital.local/api/v1/patients/PAT-OTHER-001/records"
method: "GET"
name: "IDOR-Patient-Records"
responseCodeAccept: "403"
- url: "https://api.hospital.local/api/v1/patients/PAT-OTHER-001/lab-results"
method: "GET"
name: "IDOR-Lab-Results"
responseCodeAccept: "403"
# Step 4: Report generation
- type: report
parameters:
template: "sarif-json"
reportDir: "/zap/reports"
reportFile: "healthcare-zap-report"
risks:
- high
- medium
- low
- info
3.2。醫療保健特定的 DAST 測試用例
// SecurityTestSuite.java — Integration tests for healthcare security
@QuarkusTest
@TestSecurity(user = "dr.nguyen", roles = {"doctor"})
public class HealthcareSecurityTestSuite {
@Inject
SecurityTestClient securityClient;
// ================================================================
// IDOR Tests — Insecure Direct Object Reference
// ================================================================
/**
* Test: Bác sĩ A không thể xem bệnh án của bệnh nhân
* thuộc bác sĩ B (khác khoa)
*/
@Test
@TestSecurity(user = "dr.nguyen", roles = {"doctor"},
attributes = @SecurityAttribute(key = "department", value = "cardiology"))
void testIDOR_doctorCannotAccessOtherDepartmentPatient() {
// Patient thuộc khoa Thần kinh, dr.nguyen thuộc khoa Tim mạch
Response response = given()
.header("Authorization", "Bearer " + getTokenForDrNguyen())
.when()
.get("/api/v1/patients/PAT-NEURO-001/medical-records")
.then()
.statusCode(403)
.extract().response();
// Verify audit log recorded the access attempt
AuditEntry audit = auditService.getLatestEntry();
assertThat(audit.getAction()).isEqualTo("ACCESS_DENIED");
assertThat(audit.getResourceType()).isEqualTo("MedicalRecord");
assertThat(audit.getReason()).contains("department_mismatch");
}
/**
* Test: Bệnh nhân A không thể xem bệnh án bệnh nhân B
*/
@Test
@TestSecurity(user = "patient-001", roles = {"patient"})
void testIDOR_patientCannotAccessOtherPatientRecords() {
given()
.when()
.get("/api/v1/patients/PAT-002/records")
.then()
.statusCode(403);
}
// ================================================================
// Injection Tests
// ================================================================
/**
* Test: SQL Injection trên search endpoint
*/
@Test
void testSQLInjection_patientSearch() {
String[] payloads = {
"' OR '1'='1",
"'; DROP TABLE patients; --",
"' UNION SELECT * FROM patients --",
"1' AND (SELECT COUNT(*) FROM pg_tables) > 0 --"
};
for (String payload : payloads) {
given()
.queryParam("name", payload)
.when()
.get("/api/v1/patients/search")
.then()
.statusCode(anyOf(is(400), is(200)))
.body("$", not(hasKey("pg_tables")))
.body("size()", lessThanOrEqualTo(0));
}
}
/**
* Test: XSS trong patient notes
*/
@Test
void testXSS_patientNotes() {
String[] xssPayloads = {
"<script>alert('xss')</script>",
"<img src=x onerror=alert(1)>",
"javascript:alert(document.cookie)",
"<svg/onload=alert('XSS')>"
};
for (String payload : xssPayloads) {
PatientNote note = new PatientNote();
note.setContent(payload);
String responseBody = given()
.contentType(ContentType.JSON)
.body(note)
.when()
.post("/api/v1/patients/PAT-001/notes")
.then()
.statusCode(201)
.extract().body().asString();
// Verify output encoding
assertThat(responseBody).doesNotContain("<script>");
assertThat(responseBody).doesNotContain("onerror=");
assertThat(responseBody).doesNotContain("javascript:");
}
}
// ================================================================
// Authentication & Authorization Tests
// ================================================================
/**
* Test: Expired token rejected
*/
@Test
void testAuth_expiredTokenRejected() {
String expiredToken = generateExpiredToken();
given()
.header("Authorization", "Bearer " + expiredToken)
.when()
.get("/api/v1/patients/PAT-001")
.then()
.statusCode(401);
}
/**
* Test: Token with tampered claims rejected
*/
@Test
void testAuth_tamperedTokenRejected() {
String validToken = getValidToken();
// Modify role claim from "nurse" to "admin"
String tamperedToken = tamperTokenClaim(validToken, "roles", "admin");
given()
.header("Authorization", "Bearer " + tamperedToken)
.when()
.get("/api/v1/admin/users")
.then()
.statusCode(401); // Signature verification fails
}
/**
* Test: Missing MFA for sensitive operations
*/
@Test
@TestSecurity(user = "dr.nguyen", roles = {"doctor"})
void testAuth_sensitiveOpRequiresMFA() {
// Prescription creation requires MFA (amr claim must include "mfa")
String tokenWithoutMFA = getTokenWithoutMFAClaim();
given()
.header("Authorization", "Bearer " + tokenWithoutMFA)
.contentType(ContentType.JSON)
.body(createPrescriptionPayload())
.when()
.post("/api/v1/prescriptions")
.then()
.statusCode(403)
.body("error", containsString("MFA_REQUIRED"));
}
// ================================================================
// Data Leakage Tests
// ================================================================
/**
* Test: Error responses don't leak sensitive info
*/
@Test
void testDataLeakage_errorResponseSanitized() {
Response response = given()
.queryParam("id", "invalid-uuid-format")
.when()
.get("/api/v1/patients/not-a-uuid")
.then()
.statusCode(400)
.extract().response();
String body = response.body().asString();
// Must not expose stack traces
assertThat(body).doesNotContain("java.lang");
assertThat(body).doesNotContain("at io.quarkus");
assertThat(body).doesNotContain("SQLException");
// Must not expose database details
assertThat(body).doesNotContain("postgresql");
assertThat(body).doesNotContain("pg_catalog");
assertThat(body).doesNotContain("SELECT");
}
/**
* Test: Headers don't leak technology info
*/
@Test
void testDataLeakage_noTechInfoInHeaders() {
Response response = given()
.when()
.get("/api/v1/health")
.then()
.extract().response();
Headers headers = response.headers();
assertThat(headers.hasHeaderWithName("X-Powered-By")).isFalse();
assertThat(headers.hasHeaderWithName("Server")).isFalse();
// Security headers must be present
assertThat(headers.getValue("X-Content-Type-Options"))
.isEqualTo("nosniff");
assertThat(headers.getValue("X-Frame-Options"))
.isEqualTo("DENY");
assertThat(headers.getValue("Strict-Transport-Security"))
.isNotNull();
}
}
3.3。用於醫療保健的 Nuclei 模板
# nuclei-templates/healthcare/hipaa-checks.yaml
id: hipaa-transmission-security
info:
name: HIPAA Transmission Security Check
author: xdev-security
severity: critical
description: >-
Verify TLS configuration meets HIPAA §164.312(e)(1)
Transmission Security requirements.
tags: hipaa,tls,healthcare
classification:
cwe-id: CWE-319
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
requests:
- method: GET
path:
- "{{BaseURL}}"
matchers-condition: or
matchers:
# Fail if using weak TLS
- type: dsl
name: weak-tls
dsl:
- "ssl_version == 'tls10' || ssl_version == 'tls11'"
condition: or
# Fail if no HSTS header
- type: word
name: missing-hsts
words:
- "Strict-Transport-Security"
negative: true
part: header
---
id: hipaa-session-timeout
info:
name: HIPAA Auto Logoff Check
author: xdev-security
severity: high
description: >-
Verify session timeout is configured (max 15 minutes for healthcare).
HIPAA §164.312(a)(2)(iii) — Automatic Logoff.
tags: hipaa,session,healthcare
requests:
- method: POST
path:
- "{{BaseURL}}/realms/hospital/protocol/openid-connect/token"
body: "grant_type=client_credentials&client_id={{client_id}}&client_secret={{client_secret}}"
headers:
Content-Type: application/x-www-form-urlencoded
extractors:
- type: json
name: access_token
json:
- ".access_token"
matchers:
- type: json
json:
- ".expires_in"
condition: and
# Session > 900 seconds (15 min) = violation
dsl:
- "expires_in > 900"
4. 軟體構成分析(SCA)
4.1。 Quarkus Healthcare 的依賴性掃描
# .github/workflows/healthcare-security-scan.yml
name: Healthcare Security Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
# Daily scan at midnight — catch new CVEs
- cron: '0 0 * * *'
jobs:
sca-scan:
name: Dependency Vulnerability Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
# Healthcare: fail on any HIGH or CRITICAL
exit-code: '1'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
- name: Snyk Security Scan
uses: snyk/actions/maven@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: >-
--severity-threshold=high
--policy-path=.snyk
--json-file-output=snyk-results.json
- name: Check for known healthcare CVEs
run: |
echo "=== Checking critical healthcare dependencies ==="
# Check Keycloak version for known auth bypass CVEs
KC_VERSION=$(mvn help:evaluate -Dexpression=keycloak.version -q -DforceStdout)
echo "Keycloak version: $KC_VERSION"
# Check PostgreSQL driver
PG_VERSION=$(mvn help:evaluate -Dexpression=postgresql.version -q -DforceStdout)
echo "PostgreSQL driver version: $PG_VERSION"
# Check BouncyCastle (crypto library)
BC_VERSION=$(mvn dependency:tree | grep -o 'bcprov-jdk[0-9]*:[0-9.]*' | head -1)
echo "BouncyCastle version: $BC_VERSION"
# Check for HAPI FHIR CVEs
HAPI_VERSION=$(mvn help:evaluate -Dexpression=hapi.fhir.version -q -DforceStdout)
echo "HAPI FHIR version: $HAPI_VERSION"
sast-scan:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep with healthcare rules
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/java
p/security-audit
.semgrep/healthcare-rules.yaml
- name: SonarQube Analysis
uses: SonarSource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >-
-Dsonar.qualitygate.wait=true
dast-scan:
name: Dynamic Security Testing
runs-on: ubuntu-latest
needs: [sca-scan, sast-scan]
steps:
- uses: actions/checkout@v4
- name: Start Healthcare Services
run: docker compose -f docker-compose.test.yml up -d
- name: Wait for services
run: |
timeout 120 bash -c 'until curl -sf http://localhost:8080/q/health; do sleep 2; done'
- name: Run OWASP ZAP
uses: zaproxy/[email protected]
with:
target: 'http://localhost:8080/q/openapi'
rules_file_name: 'zap-healthcare-rules.tsv'
cmd_options: '-c zap-healthcare-scan.yaml'
- name: Run Nuclei Healthcare Checks
uses: projectdiscovery/nuclei-action@main
with:
target: 'http://localhost:8080'
templates: 'nuclei-templates/healthcare/'
flags: '-severity critical,high -json -output nuclei-results.json'
4.2。 SBOM生成
# Generate Software Bill of Materials for healthcare compliance
- name: Generate SBOM (CycloneDX)
run: |
mvn org.cyclonedx:cyclonedx-maven-plugin:makeBom \
-DincludeLicenseText=true \
-DoutputFormat=json \
-DoutputName=healthcare-sbom
- name: Validate SBOM completeness
run: |
# Healthcare SBOM must include all transitive dependencies
COMPONENT_COUNT=$(jq '.components | length' target/healthcare-sbom.json)
echo "Total components in SBOM: $COMPONENT_COUNT"
# Verify critical libraries are documented
jq -r '.components[] | select(
.group == "io.quarkus" or
.group == "org.keycloak" or
.group == "org.postgresql" or
.group == "org.bouncycastle"
) | "\(.group):\(.name):\(.version)"' target/healthcare-sbom.json
5.PostgreSQL 安全審計
5.1。 PostgreSQL 安全評估腳本
-- postgresql-security-audit.sql
-- Comprehensive security audit for healthcare PostgreSQL instances
-- ================================================================
-- 1. Authentication & Connection Security
-- ================================================================
-- Check authentication methods
SELECT
line_number,
type,
database,
user_name,
address,
auth_method,
CASE
WHEN auth_method = 'trust' THEN '❌ CRITICAL — trust = no authentication!'
WHEN auth_method = 'md5' THEN '⚠️ WARNING — md5 is weak, use scram-sha-256'
WHEN auth_method = 'password' THEN '❌ CRITICAL — plaintext password!'
WHEN auth_method = 'scram-sha-256' THEN '✅ OK'
WHEN auth_method = 'cert' THEN '✅ EXCELLENT — certificate auth'
ELSE '⚠️ Review required'
END AS assessment
FROM pg_hba_file_rules
ORDER BY line_number;
-- Check SSL status
SELECT
name,
setting,
CASE
WHEN name = 'ssl' AND setting = 'on' THEN '✅ SSL enabled'
WHEN name = 'ssl' AND setting = 'off' THEN '❌ CRITICAL — SSL disabled! HIPAA violation'
WHEN name = 'ssl_min_protocol_version' AND setting IN ('TLSv1', 'TLSv1.1')
THEN '❌ Weak TLS version'
WHEN name = 'ssl_min_protocol_version' AND setting = 'TLSv1.2' THEN '✅ TLS 1.2+'
WHEN name = 'ssl_ciphers' THEN '⚠️ Review cipher suite'
ELSE setting
END AS assessment
FROM pg_settings
WHERE name IN (
'ssl', 'ssl_min_protocol_version', 'ssl_ciphers',
'ssl_cert_file', 'ssl_key_file', 'ssl_ca_file'
);
-- ================================================================
-- 2. Role & Privilege Audit
-- ================================================================
-- Superuser accounts (should be minimal)
SELECT
rolname,
rolsuper,
rolcreaterole,
rolcreatedb,
rolbypassrls,
rolvaliduntil,
CASE
WHEN rolsuper THEN '❌ SUPERUSER — minimize these!'
WHEN rolbypassrls THEN '❌ BYPASSES RLS — critical for healthcare!'
WHEN rolcreaterole THEN '⚠️ Can create roles'
ELSE '✅ OK'
END AS assessment
FROM pg_roles
WHERE rolname NOT LIKE 'pg_%'
ORDER BY rolsuper DESC, rolcreaterole DESC;
-- Check roles that bypass RLS (critical for healthcare)
SELECT rolname, rolbypassrls
FROM pg_roles
WHERE rolbypassrls = true
AND rolname NOT LIKE 'pg_%';
-- Expected: Only postgres superuser should bypass RLS
-- ================================================================
-- 3. Row-Level Security Status
-- ================================================================
-- Check which tables have RLS enabled
SELECT
schemaname,
tablename,
rowsecurity AS rls_enabled,
CASE
WHEN tablename IN ('patients', 'medical_records', 'lab_results',
'prescriptions', 'encounters', 'observations',
'diagnostic_reports')
AND rowsecurity = false
THEN '❌ CRITICAL — PHI table without RLS!'
WHEN rowsecurity = true THEN '✅ RLS enabled'
ELSE '— Non-PHI table'
END AS assessment
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY rls_enabled, schemaname, tablename;
-- Check RLS policies
SELECT
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual AS using_expression,
with_check AS check_expression
FROM pg_policies
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY schemaname, tablename, policyname;
-- ================================================================
-- 4. Encryption Audit
-- ================================================================
-- Check if pgcrypto is installed
SELECT
extname,
extversion,
'✅ Encryption extension available' AS status
FROM pg_extension
WHERE extname IN ('pgcrypto', 'pgsodium');
-- Find potentially unencrypted PHI columns
SELECT
c.table_schema,
c.table_name,
c.column_name,
c.data_type,
CASE
WHEN c.column_name IN ('ssn', 'social_security_number', 'insurance_id',
'diagnosis_code', 'diagnosis_text',
'genetic_data', 'mental_health_notes',
'substance_abuse_notes', 'hiv_status')
AND c.data_type NOT IN ('bytea')
THEN '❌ Sensitive field may need column encryption'
ELSE '—'
END AS assessment
FROM information_schema.columns c
WHERE c.table_schema NOT IN ('pg_catalog', 'information_schema')
AND c.column_name ~* '(ssn|diagnosis|genetic|mental|substance|hiv|insurance)'
ORDER BY c.table_schema, c.table_name;
-- ================================================================
-- 5. Audit Logging (pgAudit)
-- ================================================================
-- Check pgAudit configuration
SELECT
name,
setting,
CASE
WHEN name = 'pgaudit.log' AND setting = 'none'
THEN '❌ Audit logging disabled!'
WHEN name = 'pgaudit.log' AND setting LIKE '%read%'
THEN '✅ Read audit enabled'
WHEN name = 'pgaudit.log_relation' AND setting = 'on'
THEN '✅ Relation-level audit'
ELSE setting
END AS assessment
FROM pg_settings
WHERE name LIKE 'pgaudit%';
-- ================================================================
-- 6. Connection & Session Security
-- ================================================================
-- Active connections audit
SELECT
pid,
usename,
client_addr,
ssl,
application_name,
state,
query_start,
CASE
WHEN ssl = false THEN '❌ Non-SSL connection!'
WHEN usename = 'postgres' THEN '⚠️ Direct superuser connection'
ELSE '✅ OK'
END AS assessment
FROM pg_stat_ssl
JOIN pg_stat_activity USING (pid)
WHERE pid <> pg_backend_pid()
ORDER BY ssl, usename;
-- Password expiry check
SELECT
rolname,
rolvaliduntil,
CASE
WHEN rolvaliduntil IS NULL THEN '⚠️ No password expiry set'
WHEN rolvaliduntil < NOW() THEN '❌ Password expired!'
WHEN rolvaliduntil < NOW() + INTERVAL '30 days' THEN '⚠️ Expiring soon'
ELSE '✅ OK'
END AS assessment
FROM pg_roles
WHERE rolcanlogin = true
AND rolname NOT LIKE 'pg_%'
ORDER BY rolvaliduntil;
6.Keycloak 安全評估
6.1。 Keycloak配置審計
// KeycloakSecurityAudit.java
// Automated Keycloak security assessment for healthcare
@ApplicationScoped
public class KeycloakSecurityAuditor {
@Inject
Keycloak keycloakAdmin;
@ConfigProperty(name = "keycloak.realm")
String realmName;
/**
* Comprehensive Keycloak security audit for healthcare realm
*/
public SecurityAuditReport auditRealm() {
var report = new SecurityAuditReport();
RealmRepresentation realm = keycloakAdmin.realm(realmName).toRepresentation();
// 1. Brute Force Protection
auditBruteForceProtection(realm, report);
// 2. Password Policy
auditPasswordPolicy(realm, report);
// 3. Session Configuration
auditSessionConfig(realm, report);
// 4. Token Configuration
auditTokenConfig(realm, report);
// 5. Client Security
auditClientSecurity(report);
// 6. MFA Configuration
auditMFAConfig(realm, report);
// 7. CORS & Security Headers
auditSecurityHeaders(report);
return report;
}
private void auditBruteForceProtection(RealmRepresentation realm,
SecurityAuditReport report) {
if (!realm.isBruteForceProtected()) {
report.addCritical("BRUTE_FORCE",
"Brute force protection is DISABLED. " +
"Healthcare systems must prevent credential stuffing attacks. " +
"HIPAA §164.312(d) — Person or Entity Authentication.");
} else {
if (realm.getMaxFailureWaitSeconds() < 900) { // 15 min
report.addWarning("BRUTE_FORCE_WAIT",
"Lockout duration is less than 15 minutes. " +
"Healthcare recommendation: 15-30 minute lockout.");
}
if (realm.getFailureFactor() > 5) {
report.addWarning("BRUTE_FORCE_THRESHOLD",
"Failure threshold is > 5. " +
"Healthcare recommendation: max 5 failed attempts.");
}
}
}
private void auditPasswordPolicy(RealmRepresentation realm,
SecurityAuditReport report) {
String policy = realm.getPasswordPolicy();
if (policy == null || policy.isEmpty()) {
report.addCritical("PASSWORD_POLICY",
"No password policy configured! " +
"HIPAA requires strong password controls.");
return;
}
Map<String, String> policies = parsePasswordPolicy(policy);
// Minimum length
int minLength = Integer.parseInt(policies.getOrDefault("length", "0"));
if (minLength < 12) {
report.addWarning("PASSWORD_LENGTH",
"Password minimum length is " + minLength +
". Healthcare recommendation: 12+ characters.");
}
// Password history
if (!policies.containsKey("passwordHistory")) {
report.addWarning("PASSWORD_HISTORY",
"Password history not enforced. Users can reuse old passwords.");
}
// Complexity requirements
if (!policies.containsKey("upperCase") || !policies.containsKey("specialChars")) {
report.addWarning("PASSWORD_COMPLEXITY",
"Missing complexity requirements (uppercase, special chars).");
}
}
private void auditSessionConfig(RealmRepresentation realm,
SecurityAuditReport report) {
// HIPAA §164.312(a)(2)(iii) — Automatic Logoff
int ssoSessionIdle = realm.getSsoSessionIdleTimeout();
int ssoSessionMax = realm.getSsoSessionMaxLifespan();
if (ssoSessionIdle > 900) { // 15 minutes
report.addCritical("SESSION_IDLE",
"SSO session idle timeout is " + (ssoSessionIdle / 60) + " minutes. " +
"HIPAA requires auto-logoff. Healthcare max: 15 minutes.");
}
if (ssoSessionMax > 28800) { // 8 hours
report.addWarning("SESSION_MAX",
"SSO session max lifespan is " + (ssoSessionMax / 3600) + " hours. " +
"Healthcare recommendation: max 8 hours (shift duration).");
}
}
private void auditTokenConfig(RealmRepresentation realm,
SecurityAuditReport report) {
int accessTokenLifespan = realm.getAccessTokenLifespan();
if (accessTokenLifespan > 300) { // 5 minutes
report.addWarning("TOKEN_LIFESPAN",
"Access token lifespan is " + (accessTokenLifespan / 60) + " minutes. " +
"Healthcare recommendation: 5 minutes max.");
}
}
private void auditClientSecurity(SecurityAuditReport report) {
List<ClientRepresentation> clients = keycloakAdmin.realm(realmName)
.clients().findAll();
for (ClientRepresentation client : clients) {
if (client.isServiceAccountsEnabled() && client.isPublicClient()) {
report.addCritical("CLIENT_PUBLIC_SA",
"Client '" + client.getClientId() + "' is public AND has " +
"service accounts. This is a severe misconfiguration.");
}
if ("*".equals(String.join(",",
client.getRedirectUris() != null ? client.getRedirectUris() : List.of()))) {
report.addCritical("CLIENT_REDIRECT_WILDCARD",
"Client '" + client.getClientId() + "' has wildcard redirect URI. " +
"This enables open redirect attacks.");
}
if (!client.isFullScopeAllowed() == false &&
client.getDefaultClientScopes() != null &&
client.getDefaultClientScopes().size() > 10) {
report.addWarning("CLIENT_EXCESSIVE_SCOPES",
"Client '" + client.getClientId() + "' has many default scopes. " +
"Apply principle of least privilege.");
}
}
}
private void auditMFAConfig(RealmRepresentation realm,
SecurityAuditReport report) {
// Check if OTP is available
List<AuthenticationFlowRepresentation> flows = keycloakAdmin
.realm(realmName).flows().getFlows();
boolean hasMFA = flows.stream()
.anyMatch(f -> f.getAlias().toLowerCase().contains("otp") ||
f.getAlias().toLowerCase().contains("mfa") ||
f.getAlias().toLowerCase().contains("webauthn"));
if (!hasMFA) {
report.addCritical("MFA_MISSING",
"No MFA authentication flow detected. " +
"Healthcare systems handling ePHI must implement MFA. " +
"HIPAA §164.312(d) — Person or Entity Authentication.");
}
}
}
7. API 安全測試
7.1。 FHIR API 安全測試
// FHIRAPISecurityTest.java
@QuarkusTest
public class FHIRAPISecurityTest {
/**
* Test: FHIR search should not leak patient data across tenants
*/
@Test
void testFHIR_searchIsolation() {
// Token for Hospital A
String hospitalAToken = getTokenForHospital("hospital-a");
// Create patient in Hospital A
given()
.header("Authorization", "Bearer " + hospitalAToken)
.contentType("application/fhir+json")
.body(createFHIRPatient("Nguyen Van A", "hospital-a"))
.when()
.post("/fhir/Patient")
.then()
.statusCode(201);
// Token for Hospital B — should NOT see Hospital A's patients
String hospitalBToken = getTokenForHospital("hospital-b");
String result = given()
.header("Authorization", "Bearer " + hospitalBToken)
.when()
.get("/fhir/Patient?name=Nguyen")
.then()
.statusCode(200)
.extract().body().asString();
// Hospital B must not see Hospital A's patient
assertThat(result).doesNotContain("hospital-a");
assertThat(result).doesNotContain("Nguyen Van A");
}
/**
* Test: FHIR _include cannot be used to traverse to unauthorized resources
*/
@Test
void testFHIR_includeDoesNotLeakData() {
String nurseToken = getTokenForRole("nurse", "ward-3");
// Nurse from Ward 3 tries to include all Practitioners
String result = given()
.header("Authorization", "Bearer " + nurseToken)
.when()
.get("/fhir/Encounter?_include=Encounter:participant&ward=ward-3")
.then()
.statusCode(200)
.extract().body().asString();
// Should only see encounters and practitioners for ward-3
// Count shouldn't include practitioners from other wards
var bundle = parseFHIRBundle(result);
bundle.getEntry().stream()
.filter(e -> e.getResource() instanceof Encounter)
.forEach(e -> {
Encounter enc = (Encounter) e.getResource();
assertThat(enc.getLocationFirstRep().getLocation()
.getDisplay()).contains("Ward 3");
});
}
/**
* Test: Rate limiting per client
*/
@Test
void testAPI_rateLimiting() {
String token = getTokenForRole("doctor", "general");
int successCount = 0;
int rateLimitedCount = 0;
// Send 200 requests rapidly
for (int i = 0; i < 200; i++) {
int status = given()
.header("Authorization", "Bearer " + token)
.when()
.get("/api/v1/patients/PAT-001")
.then()
.extract().statusCode();
if (status == 200) successCount++;
if (status == 429) rateLimitedCount++;
}
// Should have been rate limited at some point
assertThat(rateLimitedCount).isGreaterThan(0);
// Rate limit header should be present
}
}
8. 合規性掃描
8.1。 HIPAA 合規性掃描儀
// HIPAAComplianceScanner.java
@ApplicationScoped
public class HIPAAComplianceScanner {
/**
* Automated HIPAA Technical Safeguards compliance check
*/
public HIPAAComplianceReport scan() {
var report = new HIPAAComplianceReport();
// §164.312(a)(1) — Access Control
checkAccessControl(report);
// §164.312(a)(2)(i) — Unique User Identification
checkUniqueUserID(report);
// §164.312(a)(2)(iii) — Automatic Logoff
checkAutoLogoff(report);
// §164.312(a)(2)(iv) — Encryption and Decryption
checkEncryption(report);
// §164.312(b) — Audit Controls
checkAuditControls(report);
// §164.312(c)(1) — Integrity
checkIntegrity(report);
// §164.312(d) — Person or Entity Authentication
checkAuthentication(report);
// §164.312(e)(1) — Transmission Security
checkTransmissionSecurity(report);
return report;
}
private void checkEncryption(HIPAAComplianceReport report) {
var check = new ComplianceCheck("§164.312(a)(2)(iv)",
"Encryption and Decryption", "Required");
// Check 1: TDE or column encryption for ePHI at rest
boolean atRestEncryption = verifyDatabaseEncryption();
check.addResult("Data at rest encryption", atRestEncryption,
atRestEncryption ? "pgcrypto/TDE active" : "No at-rest encryption detected");
// Check 2: TLS for data in transit
boolean inTransitEncryption = verifyTLSConfiguration();
check.addResult("Data in transit encryption", inTransitEncryption,
inTransitEncryption ? "TLS 1.2+ enforced" : "TLS not properly configured");
// Check 3: Key management
boolean keyManagement = verifyKeyManagement();
check.addResult("Key management via KMS", keyManagement,
keyManagement ? "Vault KMS active" : "No centralized key management");
report.addCheck(check);
}
private void checkAuditControls(HIPAAComplianceReport report) {
var check = new ComplianceCheck("§164.312(b)",
"Audit Controls", "Required");
// Check 1: pgAudit enabled
boolean pgAuditEnabled = verifyPgAudit();
check.addResult("Database audit logging", pgAuditEnabled,
pgAuditEnabled ? "pgAudit active" : "pgAudit not detected");
// Check 2: Application audit logging
boolean appAudit = verifyApplicationAuditLogging();
check.addResult("Application audit trail", appAudit,
appAudit ? "Structured audit logging active" : "Missing audit logging");
// Check 3: Immutable log storage
boolean immutableLogs = verifyImmutableLogStorage();
check.addResult("Immutable log storage", immutableLogs,
immutableLogs ? "Write-once storage configured" : "Logs may be tampered");
// Check 4: Log retention (6 years for HIPAA)
boolean logRetention = verifyLogRetention();
check.addResult("6-year log retention", logRetention,
logRetention ? "Retention policy configured" : "Retention not verified");
report.addCheck(check);
}
}
8.2。合規報告模板
HIPAA 技術保障 — 合規性評估報告
| 領域 | 價值 |
|---|---|
| 系統 | 醫療保健微服務平台 |
| 日期 | 2026 年第一季 |
| 評審員 | 安全團隊 |
| 範圍 | Quarkus API + PostgreSQL + Keycloak |
§164.312(a)(1) — 存取控制:
- 唯一的使用者 ID — ✅ PASS
- 緊急通道程序 — ✅ 通過
- 自動登出(15 分鐘)— ✅ 透過
- 加密/解密 — ✅ 透過
§164.312(b) — 審計控制:
- 啟用 pgAudit — ✅ 透過
- 應用程式審核追蹤 — ✅ 透過
- 不可變的日誌儲存 — ⚠️ PARTIAL
- 6 年保留 — ✅ 通過
§164.312(c)(1) — 完整性: ePHI 完整性機制 ✅、數位簽章 ✅
§164.312(d) — 驗證: 用於 ePHI 存取的 MFA ✅、密碼策略 ✅、基於憑證的驗證 ✅
§164.312(e)(1) — 傳輸安全: 強制執行 TLS 1.2+、mTLS 服務間 ✅、停用弱密碼套件 ✅
總分: 95% 符合(1 部分結果)| **風險等級:**低| 下一次回顧: 2026 年第二季度
9. 整合 CI/CD 安全管道
9.1。完整的安全管道
# .github/workflows/healthcare-full-security.yml
name: Healthcare Complete Security Pipeline
on:
pull_request:
branches: [main]
jobs:
security-gate:
name: Security Quality Gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# === SAST ===
- name: Semgrep SAST
uses: returntocorp/semgrep-action@v1
with:
config: .semgrep/healthcare-rules.yaml
# === SCA ===
- name: Trivy Dependency Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
severity: CRITICAL,HIGH
exit-code: 1
# === Secrets Detection ===
- name: Gitleaks Secret Detection
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# === Container Scan ===
- name: Build Container
run: docker build -t healthcare-app:test .
- name: Scan Container Image
uses: aquasecurity/trivy-action@master
with:
image-ref: healthcare-app:test
severity: CRITICAL,HIGH
exit-code: 1
# === SBOM ===
- name: Generate SBOM
run: |
mvn org.cyclonedx:cyclonedx-maven-plugin:makeBom
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: target/bom.json
# === Compliance Check ===
- name: HIPAA Compliance Scan
run: |
mvn test -Dtest=HIPAAComplianceScanner \
-Dquarkus.profile=security-audit
- name: Summary
if: always()
run: |
echo "## Healthcare Security Gate Results" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| SAST (Semgrep) | ✅ |" >> $GITHUB_STEP_SUMMARY
echo "| SCA (Trivy) | ✅ |" >> $GITHUB_STEP_SUMMARY
echo "| Secrets (Gitleaks) | ✅ |" >> $GITHUB_STEP_SUMMARY
echo "| Container Scan | ✅ |" >> $GITHUB_STEP_SUMMARY
echo "| SBOM Generated | ✅ |" >> $GITHUB_STEP_SUMMARY
echo "| HIPAA Compliance | ✅ |" >> $GITHUB_STEP_SUMMARY
10.安全評估報告模板
10.1。醫療保健滲透測試報告結構
Healthcare Security Assessment Report
======================================
1. EXECUTIVE SUMMARY
- Scope & objectives
- Overall risk rating
- Key findings summary
- Compliance status (HIPAA, NĐ 13/2023)
2. METHODOLOGY
- Testing approach (black/grey/white box)
- Tools used
- Testing dates
- Exclusions
3. FINDINGS
For each finding:
┌─────────────────────────────────────────┐
│ Finding ID: HC-2026-001 │
│ Title: IDOR on Patient Records API │
│ Severity: CRITICAL │
│ CVSS: 9.1 │
│ CWE: CWE-639 │
│ HIPAA: §164.312(a)(1) — Access Control │
│ ──────────────────────────────────────── │
│ Description: │
│ Bác sĩ ngoại khoa có thể truy cập │
│ hồ sơ bệnh nhân khoa tâm thần bằng │
│ cách thay đổi patient ID trong URL. │
│ │
│ Impact: │
│ Rò rỉ ePHI liên khoa, vi phạm nguyên │
│ tắc minimum necessary access. │
│ │
│ Evidence: │
│ GET /api/v1/patients/PAT-PSY-003 │
│ Authorization: Bearer <cardiology_jwt> │
│ Response: 200 OK (returned PHI) │
│ │
│ Remediation: │
│ Implement department-based RLS policy │
│ in PostgreSQL + verify department │
│ claim in JWT at API layer. │
│ │
│ Status: REMEDIATED (2026-03-15) │
│ Retest: PASSED (2026-03-20) │
└─────────────────────────────────────────┘
4. COMPLIANCE MAPPING
Map each finding to HIPAA Technical Safeguards
5. RISK REGISTER UPDATE
Update Risk Register with new findings + remediation
6. APPENDICES
- Full scan outputs
- Network topology tested
- Tool configurations
- SBOM
11. 總結
醫療保健安全測試清單
┌─────────────────────────────────────────────────────────────┐
│ Healthcare Security Testing Checklist │
├─────────────────────────────────────────────────────────────┤
│ │
│ SAST (Every Commit): │
│ □ SonarQube with healthcare custom rules │
│ □ Semgrep with PHI-specific patterns │
│ □ SpotBugs + FindSecBugs │
│ │
│ SCA (Every Commit + Daily): │
│ □ Trivy filesystem scan │
│ □ Snyk dependency check │
│ □ SBOM generation (CycloneDX) │
│ □ License compliance check │
│ │
│ DAST (Every Release): │
│ □ OWASP ZAP with healthcare scan policy │
│ □ Nuclei healthcare templates │
│ □ IDOR testing on all patient endpoints │
│ □ FHIR API security validation │
│ │
│ Infrastructure (Monthly): │
│ □ PostgreSQL security audit script │
│ □ Keycloak security assessment │
│ □ CIS Kubernetes Benchmark │
│ □ Network segmentation verification │
│ │
│ Penetration Testing (Quarterly): │
│ □ External pentest by third party │
│ □ Internal assessment │
│ □ Social engineering assessment │
│ □ Physical security assessment │
│ │
│ Compliance (Annual): │
│ □ Full HIPAA Technical Safeguards audit │
│ □ Risk Analysis update (§164.308(a)(1)) │
│ □ Security Assessment Report │
│ □ Business Associate Agreement review │
└─────────────────────────────────────────────────────────────┘
| ◀ 上一篇 | 下一篇文章 ▶ |
|---|---|
| 第 22 課:醫療保健工作負載的容器和 Kubernetes 安全性 | 第 24 課:Capstone — 建立安全的醫療保健微服務平台 |