security-mcp 1.1.4 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +341 -1018
  2. package/defaults/checklists/ai.json +20 -1
  3. package/defaults/checklists/api.json +35 -1
  4. package/defaults/checklists/infra.json +34 -1
  5. package/defaults/checklists/mobile.json +23 -1
  6. package/defaults/checklists/payments.json +15 -1
  7. package/defaults/checklists/web.json +11 -1
  8. package/defaults/cloud-controls/aws.json +10712 -0
  9. package/defaults/cloud-controls/azure.json +7201 -0
  10. package/defaults/cloud-controls/gcp.json +4061 -0
  11. package/defaults/control-catalog.json +24 -0
  12. package/defaults/security-policy.json +2 -2
  13. package/dist/ci/pr-gate.js +22 -5
  14. package/dist/cli/index.js +73 -2
  15. package/dist/cli/install.js +4 -55
  16. package/dist/cli/onboarding.js +18 -10
  17. package/dist/gate/baseline.js +82 -7
  18. package/dist/gate/catalog.js +10 -2
  19. package/dist/gate/checks/agentic-instructions.js +515 -0
  20. package/dist/gate/checks/ai-governance.js +132 -0
  21. package/dist/gate/checks/ai.js +757 -39
  22. package/dist/gate/checks/auth-deep.js +920 -216
  23. package/dist/gate/checks/business-logic.js +751 -0
  24. package/dist/gate/checks/ci-pipeline.js +399 -4
  25. package/dist/gate/checks/cloud-controls.js +69 -0
  26. package/dist/gate/checks/crypto.js +423 -2
  27. package/dist/gate/checks/data-platform.js +954 -0
  28. package/dist/gate/checks/dependencies.js +582 -15
  29. package/dist/gate/checks/docker-deep.js +1236 -0
  30. package/dist/gate/checks/gitops.js +724 -0
  31. package/dist/gate/checks/graphql.js +201 -19
  32. package/dist/gate/checks/iac.js +1230 -0
  33. package/dist/gate/checks/infra.js +246 -1
  34. package/dist/gate/checks/injection-deep.js +827 -184
  35. package/dist/gate/checks/k8s.js +955 -2
  36. package/dist/gate/checks/mobile-android.js +917 -3
  37. package/dist/gate/checks/mobile-ios.js +797 -5
  38. package/dist/gate/checks/required-artifacts.js +194 -0
  39. package/dist/gate/checks/runtime.js +178 -0
  40. package/dist/gate/checks/secrets.js +256 -13
  41. package/dist/gate/checks/supply-chain-deep.js +787 -0
  42. package/dist/gate/checks/web-nextjs.js +572 -48
  43. package/dist/gate/cloud-controls/apply.js +115 -0
  44. package/dist/gate/cloud-controls/bicep.js +36 -0
  45. package/dist/gate/cloud-controls/cfn.js +125 -0
  46. package/dist/gate/cloud-controls/detect.js +104 -0
  47. package/dist/gate/cloud-controls/hcl.js +140 -0
  48. package/dist/gate/cloud-controls/types.js +87 -0
  49. package/dist/gate/diff.js +17 -5
  50. package/dist/gate/evidence.js +8 -1
  51. package/dist/gate/exceptions.js +202 -9
  52. package/dist/gate/findings.js +15 -2
  53. package/dist/gate/policy.js +316 -130
  54. package/dist/gate/threat-intel.js +6 -0
  55. package/dist/mcp/audit-chain.js +131 -28
  56. package/dist/mcp/auth.js +169 -0
  57. package/dist/mcp/learning.js +129 -4
  58. package/dist/mcp/model-router.js +161 -24
  59. package/dist/mcp/orchestration.js +377 -89
  60. package/dist/mcp/server.js +460 -69
  61. package/dist/mcp/tool-audit.js +193 -0
  62. package/dist/repo/fs.js +37 -1
  63. package/dist/repo/search.js +31 -6
  64. package/dist/review/store.js +56 -3
  65. package/dist/tests/run.js +124 -1
  66. package/package.json +9 -9
  67. package/skills/_TEMPLATE/SKILL.md +99 -0
  68. package/skills/advanced-dos-tester/SKILL.md +118 -0
  69. package/skills/agentic-instruction-auditor/SKILL.md +111 -0
  70. package/skills/agentic-loop-exploiter/SKILL.md +377 -0
  71. package/skills/ai-llm-redteam/SKILL.md +113 -0
  72. package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
  73. package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
  74. package/skills/android-penetration-tester/SKILL.md +464 -46
  75. package/skills/anti-replay-tester/SKILL.md +115 -0
  76. package/skills/appsec-code-auditor/SKILL.md +94 -0
  77. package/skills/artifact-integrity-analyst/SKILL.md +450 -0
  78. package/skills/attack-navigator/SKILL.md +476 -8
  79. package/skills/auth-session-hacker/SKILL.md +111 -0
  80. package/skills/aws-penetration-tester/SKILL.md +510 -0
  81. package/skills/azure-penetration-tester/SKILL.md +542 -3
  82. package/skills/binary-auth-validator/SKILL.md +120 -0
  83. package/skills/bot-detection-specialist/SKILL.md +118 -0
  84. package/skills/business-logic-attacker/SKILL.md +240 -0
  85. package/skills/capec-code-mapper/SKILL.md +93 -0
  86. package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
  87. package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
  88. package/skills/ciso-orchestrator/SKILL.md +465 -43
  89. package/skills/cloud-infra-specialist/SKILL.md +127 -0
  90. package/skills/compliance-gap-analyst/SKILL.md +431 -0
  91. package/skills/compliance-grc/SKILL.md +94 -0
  92. package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
  93. package/skills/container-hardening-auditor/SKILL.md +125 -0
  94. package/skills/credential-stuffing-specialist/SKILL.md +111 -0
  95. package/skills/crypto-pki-specialist/SKILL.md +96 -0
  96. package/skills/csa-ccm-mapper/SKILL.md +93 -0
  97. package/skills/csf2-governance-mapper/SKILL.md +93 -0
  98. package/skills/data-platform-auditor/SKILL.md +125 -0
  99. package/skills/deep-link-fuzzer/SKILL.md +118 -0
  100. package/skills/dependency-confusion-attacker/SKILL.md +424 -0
  101. package/skills/device-integrity-aggregator/SKILL.md +117 -0
  102. package/skills/dos-resilience-tester/SKILL.md +106 -0
  103. package/skills/dread-scorer/SKILL.md +93 -0
  104. package/skills/egress-policy-enforcer/SKILL.md +108 -0
  105. package/skills/evidence-collector/SKILL.md +107 -0
  106. package/skills/file-upload-attacker/SKILL.md +118 -0
  107. package/skills/gcp-penetration-tester/SKILL.md +510 -2
  108. package/skills/git-history-secret-scanner/SKILL.md +115 -0
  109. package/skills/gitops-delivery-auditor/SKILL.md +120 -0
  110. package/skills/iac-security-auditor/SKILL.md +125 -0
  111. package/skills/iam-privesc-graph-builder/SKILL.md +161 -0
  112. package/skills/incident-responder/SKILL.md +120 -0
  113. package/skills/injection-specialist/SKILL.md +111 -0
  114. package/skills/ios-security-auditor/SKILL.md +291 -0
  115. package/skills/json-ambiguity-tester/SKILL.md +145 -0
  116. package/skills/k8s-container-escaper/SKILL.md +406 -0
  117. package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
  118. package/skills/kill-switch-engineer/SKILL.md +111 -0
  119. package/skills/linddun-privacy-analyst/SKILL.md +111 -0
  120. package/skills/logic-race-fuzzer/SKILL.md +452 -0
  121. package/skills/mobile-api-network-attacker/SKILL.md +430 -0
  122. package/skills/mobile-binary-hardener/SKILL.md +111 -0
  123. package/skills/mobile-security-specialist/SKILL.md +94 -0
  124. package/skills/mobile-webview-auditor/SKILL.md +105 -0
  125. package/skills/model-extraction-attacker/SKILL.md +228 -0
  126. package/skills/multipart-abuse-tester/SKILL.md +93 -0
  127. package/skills/oauth-pkce-specialist/SKILL.md +113 -0
  128. package/skills/parser-exhaustion-tester/SKILL.md +151 -0
  129. package/skills/pentest-infra/SKILL.md +107 -0
  130. package/skills/pentest-social/SKILL.md +210 -0
  131. package/skills/pentest-team/SKILL.md +96 -0
  132. package/skills/pentest-web-api/SKILL.md +107 -0
  133. package/skills/privacy-flow-analyst/SKILL.md +243 -0
  134. package/skills/prompt-injection-specialist/SKILL.md +403 -0
  135. package/skills/quantum-migration-planner/SKILL.md +105 -0
  136. package/skills/rag-poisoning-specialist/SKILL.md +367 -0
  137. package/skills/registry-mirror-enforcer/SKILL.md +93 -0
  138. package/skills/rotation-validation-agent/SKILL.md +121 -0
  139. package/skills/samm-assessor/SKILL.md +94 -0
  140. package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
  141. package/skills/senior-security-engineer/SKILL.md +178 -0
  142. package/skills/serialization-memory-attacker/SKILL.md +341 -0
  143. package/skills/session-timeout-tester/SKILL.md +170 -0
  144. package/skills/slsa-level3-enforcer/SKILL.md +121 -0
  145. package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
  146. package/skills/ssrf-detection-validator/SKILL.md +117 -0
  147. package/skills/step-up-auth-enforcer/SKILL.md +93 -0
  148. package/skills/stride-pasta-analyst/SKILL.md +429 -0
  149. package/skills/supply-chain-devsecops/SKILL.md +107 -0
  150. package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
  151. package/skills/threat-modeler/SKILL.md +94 -0
  152. package/skills/tls-certificate-auditor/SKILL.md +582 -18
  153. package/skills/token-reuse-detector/SKILL.md +104 -0
  154. package/skills/trike-risk-modeler/SKILL.md +93 -0
  155. package/skills/unicode-homograph-tester/SKILL.md +93 -0
  156. package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
  157. package/skills/webhook-security-tester/SKILL.md +111 -0
  158. package/skills/zero-trust-architect/SKILL.md +118 -0
@@ -0,0 +1,954 @@
1
+ import { searchRepo } from "../../repo/search.js";
2
+ // ---------------------------------------------------------------------------
3
+ // Databricks patterns
4
+ // ---------------------------------------------------------------------------
5
+ // 1. Hardcoded Databricks PAT / token / host with embedded creds.
6
+ const DBX_HARDCODED_TOKEN_PATTERN = String.raw `dapi[0-9a-f]{16,}|` + // raw PAT value
7
+ String.raw `DATABRICKS_TOKEN\s*[=:]\s*["']?dapi|` + // env-style assignment to a PAT
8
+ String.raw `token\s*[=:]\s*["']dapi|` + // token = "dapi..."
9
+ String.raw `databricks_token\s*[=:]\s*["']dapi|` + // tf var assigned a literal PAT
10
+ String.raw `https://[^"'\s]+:[^"'@\s]+@[^"'\s]*databricks`; // host URL with embedded creds
11
+ // 2. Secret-scope misuse: result of dbutils.secrets.get printed/logged.
12
+ const DBX_SECRET_LEAK_PATTERN = String.raw `print\s*\(\s*dbutils\.secrets\.get|` +
13
+ String.raw `(?:log|logger|logging)\.[a-z]+\s*\(\s*dbutils\.secrets\.get|` +
14
+ String.raw `displayHTML\s*\(\s*dbutils\.secrets\.get|` +
15
+ String.raw `spark\.conf\.set\([^)]*dbutils\.secrets\.get`;
16
+ // 3. Weak cluster isolation / table ACLs disabled (no Unity Catalog enforcement).
17
+ const DBX_WEAK_ISOLATION_PATTERN = String.raw `data_security_mode\s*[=:]\s*["']?(?:NONE|LEGACY_[A-Z_]+)|` +
18
+ String.raw `spark\.databricks\.acl\.dfAclsEnabled["']?\s*[=:]\s*["']?false|` +
19
+ String.raw `spark\.databricks\.acl\.sqlOnly["']?\s*[=:]\s*["']?false|` +
20
+ String.raw `table_access_control_enabled\s*[=:]\s*false`;
21
+ // 4. Init scripts from DBFS / world-writable / external URL.
22
+ const DBX_INIT_SCRIPT_PATTERN = String.raw `init_scripts?\s*\{[^}]*dbfs\s*\{|` +
23
+ String.raw `"?destination"?\s*[=:]\s*["']dbfs:/|` +
24
+ String.raw `global_init_script|databricks_global_init_script|` +
25
+ String.raw `init_scripts?\s*\{[^}]*\b(?:http|https)\b`;
26
+ // 5. Public network exposure for clusters / workspace.
27
+ const DBX_PUBLIC_NETWORK_PATTERN = String.raw `enable_public_ip\s*[=:]\s*true|` +
28
+ String.raw `no_public_ip\s*[=:]\s*false|` +
29
+ String.raw `enable_no_public_ip\s*[=:]\s*false|` +
30
+ String.raw `public_access_enabled\s*[=:]\s*true|` +
31
+ String.raw `databricks_ip_access_list[^=]*enabled\s*=\s*false`;
32
+ // 6. Databricks token resource with long / no expiry, or admin service principal.
33
+ const DBX_TOKEN_RESOURCE_PATTERN = String.raw `resource\s+"databricks_token"|` +
34
+ String.raw `lifetime_seconds\s*=\s*-1|` + // never expires
35
+ String.raw `lifetime_seconds\s*=\s*\d{8,}|` + // ~years
36
+ String.raw `databricks_service_principal[^}]*allow_cluster_create|` +
37
+ String.raw `(?:databricks_(?:group|service_principal)_role|admin)\s*=\s*["']?admin`;
38
+ // 7. spark.conf.set with inline credentials / keys, or data exfil to external endpoint.
39
+ const DBX_INLINE_CREDS_PATTERN = String.raw `spark\.conf\.set\([^)]*(?:fs\.s3a\.(?:access|secret)\.key|account\.key|sas)[^)]*["'][^)]*["']\)|` +
40
+ String.raw `fs\.azure\.account\.key[^=]*=\s*["'][A-Za-z0-9+/=]{20,}|` +
41
+ String.raw `\.option\(\s*["'](?:user|password|accessKeyId|secretAccessKey)["']\s*,\s*["'][^"']+["']\)`;
42
+ // 8. Legacy hive_metastore use / Unity Catalog not in play.
43
+ const DBX_LEGACY_METASTORE_PATTERN = String.raw `hive_metastore\.|` +
44
+ String.raw `CREATE\s+TABLE\s+hive_metastore|` +
45
+ String.raw `USE\s+CATALOG\s+hive_metastore|` +
46
+ String.raw `spark\.databricks\.unityCatalog\.enabled["']?\s*[=:]\s*["']?false`;
47
+ // ---------------------------------------------------------------------------
48
+ // Snowflake patterns
49
+ // ---------------------------------------------------------------------------
50
+ // 9. Over-privileged grants.
51
+ const SF_OVERPRIV_GRANT_PATTERN = String.raw `GRANT\s+(?:ROLE\s)?(?:ACCOUNTADMIN|SECURITYADMIN|SYSADMIN)\b|` +
52
+ String.raw `GRANT\s+ALL\s+PRIVILEGES\b|` +
53
+ String.raw `GRANT\b[^;]*\bTO\s+(?:ROLE\s)?PUBLIC\b|` +
54
+ String.raw `"?role"?\s*=\s*"ACCOUNTADMIN"`;
55
+ // 10. CREATE USER with hardcoded password / no MUST_CHANGE_PASSWORD.
56
+ const SF_USER_PASSWORD_PATTERN = String.raw `CREATE\s+(?:OR\sREPLACE\s)?USER\s+[^;]*PASSWORD\s*=\s*["'][^"']+["']|` +
57
+ String.raw `snowflake_user[^}]*password\s*=\s*["'][^"']+["']|` +
58
+ String.raw `MUST_CHANGE_PASSWORD\s*=\s*FALSE`;
59
+ // 11. Auth weaknesses: no MFA / no key-pair / session keepalive.
60
+ const SF_WEAK_AUTH_PATTERN = String.raw `ALLOW_CLIENT_SET_SESSION_KEEPALIVE\s*=\s*TRUE|` +
61
+ String.raw `CLIENT_SESSION_KEEP_ALIVE\s*=\s*TRUE|` +
62
+ String.raw `disable_mfa\s*=\s*true|` +
63
+ String.raw `MINS_TO_BYPASS_MFA\s*=`;
64
+ // 12. Network policy missing or wide-open.
65
+ const SF_NETWORK_OPEN_PATTERN = String.raw `ALLOWED_IP_LIST\s*=\s*\(?\s*["']0\.0\.0\.0/0["']|` +
66
+ String.raw `ALLOWED_IP_LIST\s*=\s*\(?\s*["']\*["']|` +
67
+ String.raw `ALLOWED_IP_LIST\s*=\s*\(?\s*["']0\.0\.0\.0["']`;
68
+ // 12b. Detect presence of any network policy (absence check).
69
+ const SF_NETWORK_POLICY_PRESENT_PATTERN = String.raw `CREATE\s+(?:OR\sREPLACE\s)?NETWORK\s+POLICY|snowflake_network_policy`;
70
+ // 12c. Detect any Snowflake usage at all (so we only warn on absence when relevant).
71
+ const SF_USAGE_PATTERN = String.raw `snowflake_account|snowflakecomputing\.com|` +
72
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?(?:USER|WAREHOUSE|DATABASE|SHARE)\b|` +
73
+ String.raw `provider\s+"snowflake"`;
74
+ // 13. Hardcoded Snowflake connection creds in code / .tf.
75
+ const SF_HARDCODED_CONN_PATTERN = String.raw `snowflake_account\s*[=:]\s*["'][^"']+["']|` +
76
+ String.raw `account\s*[=:]\s*["'][a-z0-9_-]+\.snowflakecomputing|` +
77
+ String.raw `(?:password|pwd)\s*[=:]\s*["'][^"']{3,}["'][^#\n]*snowflake|` +
78
+ String.raw `snowflake[^#\n]*(?:password|pwd)\s*[=:]\s*["'][^"']{3,}["']`;
79
+ // 14. Data sharing to whole account / external stage with hardcoded AWS creds.
80
+ const SF_SHARE_STAGE_PATTERN = String.raw `ALTER\s+SHARE\s+[^;]*ADD\s+ACCOUNTS\s*=|` +
81
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?SHARE\b|` +
82
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?STAGE\b[^;]*CREDENTIALS\s*=|` +
83
+ String.raw `AWS_KEY_ID\s*=\s*["']AKIA|AWS_SECRET_KEY\s*=\s*["']`;
84
+ // 15. PII columns without masking policy (heuristic / LOW).
85
+ const SF_PII_COLUMN_PATTERN = String.raw `\b(?:ssn|social_security|credit_card|card_number|cvv|passport|date_of_birth|dob|tax_id|email|phone_number)\b[^,;\n]*(?:VARCHAR|STRING|NUMBER|CHAR|TEXT)`;
86
+ // Detects an actual masking/row-access policy being defined or attached to a column —
87
+ // deliberately excludes "GRANT APPLY MASKING POLICY …" so a privilege grant elsewhere in
88
+ // the repo does not suppress the PII-without-masking heuristic.
89
+ const SF_MASKING_PRESENT_PATTERN = String.raw `CREATE\s+(?:OR\sREPLACE\s)?MASKING\s+POLICY|` +
90
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?ROW\s+ACCESS\s+POLICY|` +
91
+ String.raw `(?:SET|WITH|ADD)\s+(?:MASKING|ROW\s+ACCESS)\s+POLICY|` +
92
+ String.raw `snowflake_masking_policy|snowflake_row_access_policy`;
93
+ // 16. ALTER ACCOUNT weakening security params.
94
+ const SF_WEAKEN_ACCOUNT_PATTERN = String.raw `ALTER\s+ACCOUNT\s+SET\s+[^;]*=\s*FALSE|` +
95
+ String.raw `REQUIRE_STORAGE_INTEGRATION_FOR_STAGE_CREATION\s*=\s*FALSE|` +
96
+ String.raw `PREVENT_UNLOAD_TO_INLINE_URL\s*=\s*FALSE|` +
97
+ String.raw `REQUIRE_STORAGE_INTEGRATION_FOR_STAGE_OPERATION\s*=\s*FALSE`;
98
+ // ---------------------------------------------------------------------------
99
+ // Databricks DEPTH patterns (Round 2)
100
+ // ---------------------------------------------------------------------------
101
+ // 17. Unity Catalog GRANT of ALL PRIVILEGES / MANAGE to the whole-account groups.
102
+ const DBX_UC_BROAD_GRANT_PATTERN = String.raw `GRANT\s+(?:ALL\s+PRIVILEGES|MANAGE|MODIFY)\b[^;]*\bTO\s+(?:\x60)?(?:account\s+users|users|all\s+account\s+users)\b|` +
103
+ String.raw `GRANT\s+ALL\s+PRIVILEGES\s+ON\s+CATALOG\b[^;]*\bTO\b|` +
104
+ String.raw `principal\s*=\s*["']?(?:account users|users)["']?[^}]*privileges\s*=\s*\[[^\]]*ALL_PRIVILEGES`;
105
+ // 18. External location / storage credential that is public / over-broad.
106
+ // searchRepo is per-line, so each alternative matches a single realistic line.
107
+ const DBX_EXTERNAL_LOCATION_PATTERN = String.raw `url\s*=\s*["']s3a?://[^"']*\*["']|` +
108
+ String.raw `skip_validation\s*=\s*true|` +
109
+ String.raw `storage_credential[^=]*=\s*["'][^"']*public|` +
110
+ String.raw `GRANT\s+(?:READ|WRITE)\s+FILES\s+ON\s+EXTERNAL\s+LOCATION\b[^;]*\bTO\s+(?:\x60)?(?:account\s+users|users)\b`;
111
+ // 19. Serverless SQL warehouse with no IP access list / public.
112
+ const DBX_SERVERLESS_NO_ACL_PATTERN = String.raw `databricks_sql_endpoint[^}]*enable_serverless_compute\s*=\s*true|` +
113
+ String.raw `enable_serverless_compute\s*=\s*true|` +
114
+ String.raw `databricks_sql_global_config[^}]*enable_serverless\s*=\s*true|` +
115
+ String.raw `warehouse_type\s*=\s*["']?PRO["']?[^}]*serverless`;
116
+ // 20. spark_conf block exposing storage account/access keys inline.
117
+ const DBX_SPARK_CONF_KEY_PATTERN = String.raw `"?fs\.azure\.account\.key[^"=]*"?\s*[=:]\s*["'][A-Za-z0-9+/=]{12,}|` +
118
+ String.raw `"?fs\.s3a\.(?:access|secret)\.key"?\s*[=:]\s*["'][A-Za-z0-9+/=]{8,}|` +
119
+ String.raw `"?spark\.hadoop\.fs\.[^"=]*\.key"?\s*[=:]\s*["'][^"']{8,}`;
120
+ // 21. Single-user / shared no-isolation mode mismatch.
121
+ // Single-line signal: a SINGLE_USER assignment with an empty single_user_name, or a
122
+ // single_user_name paired with NONE on the same line.
123
+ const DBX_SINGLE_USER_MISMATCH_PATTERN = String.raw `data_security_mode\s*=\s*["']?SINGLE_USER["']?\s*,?\s*single_user_name\s*=\s*["']{2}|` +
124
+ String.raw `single_user_name\s*=\s*["']{2}|` +
125
+ String.raw `single_user_name\s*=\s*["'][^"']+["']\s*data_security_mode\s*=\s*["']?(?:NONE|USER_ISOLATION)|` +
126
+ String.raw `single_user_isolation_mismatch\s*=\s*true`;
127
+ // 22. Model serving endpoint public with no auth (per-line signals).
128
+ const DBX_MODEL_SERVING_PUBLIC_PATTERN = String.raw `\b(?:auth|access_control)\s*=\s*["']none["']|` +
129
+ String.raw `serving_endpoint_public\s*=\s*true|` +
130
+ String.raw `serving\.endpoints[^=]*group_name\s*=\s*["']users["']|` +
131
+ String.raw `databricks_(?:model_serving|serving_endpoint)\b.*public\s*=\s*true`;
132
+ // 23. databricks_permissions granting CAN_MANAGE to the users group (per-line).
133
+ const DBX_PERMISSIONS_CAN_MANAGE_PATTERN = String.raw `group_name\s*=\s*["']users["']\s*,?\s*permission_level\s*=\s*["']CAN_MANAGE["']|` +
134
+ String.raw `permission_level\s*=\s*["']CAN_MANAGE["']\s*,?\s*group_name\s*=\s*["']users["']|` +
135
+ String.raw `group_name\s*=\s*["']account users["']|` +
136
+ String.raw `permissions_can_manage_users\s*=\s*true`;
137
+ // 24. Repos / git credential with inline PAT.
138
+ const DBX_GIT_CREDENTIAL_PATTERN = String.raw `databricks_git_credential[^}]*personal_access_token\s*=\s*["'][^"']{8,}|` +
139
+ String.raw `git_provider\s*=\s*["'][a-zA-Z]+["'][^}]*personal_access_token\s*=\s*["']gh[pous]_|` +
140
+ String.raw `personal_access_token\s*=\s*["'](?:gh[pous]_|glpat-|dapi)`;
141
+ // 25. Jobs with run_as elevated service principal / admin (per-line).
142
+ const DBX_JOB_RUN_AS_PATTERN = String.raw `run_as\s*\{\s*service_principal_name\s*=|` +
143
+ String.raw `service_principal_name\s*=\s*["'][^"']*(?:admin|Admin|sp)["']|` +
144
+ String.raw `run_as[^=]*user_name\s*=\s*["'][^"']*admin|` +
145
+ String.raw `"run_as_owner"\s*:\s*true`;
146
+ // 26. DBFS mount with inline storage key.
147
+ const DBX_DBFS_MOUNT_KEY_PATTERN = String.raw `dbutils\.fs\.mount\([^)]*(?:fs\.azure\.account\.key|fs\.s3a\.(?:access|secret)\.key)|` +
148
+ String.raw `dbutils\.fs\.mount\([^)]*["'](?:AKIA[A-Z0-9]{8,}|[A-Za-z0-9+/=]{24,})["']|` +
149
+ String.raw `extra_configs\s*=\s*\{[^}]*account\.key[^}]*["'][A-Za-z0-9+/=]{12,}`;
150
+ // 27. Overprivileged instance profile ARN attached to clusters.
151
+ const DBX_INSTANCE_PROFILE_PATTERN = String.raw `instance_profile_arn\s*=\s*["']arn:aws:iam::[0-9]+:instance-profile/[^"']*(?:admin|Admin|PowerUser|FullAccess)|` +
152
+ String.raw `databricks_instance_profile[^}]*iam_role_arn\s*=\s*["']arn:aws:iam::[0-9]+:role/[^"']*(?:admin|Admin)|` +
153
+ String.raw `instance_profile_arn\s*=\s*["']arn:aws:iam::[0-9]+:instance-profile/[^"']*\*`;
154
+ // 28. Workspace conf weakening: tokens/dbfs-browser enabled, user isolation off.
155
+ const DBX_WORKSPACE_CONF_PATTERN = String.raw `enableTokensConfig["']?\s*[=:]\s*["']?true|` +
156
+ String.raw `enableDbfsFileBrowser["']?\s*[=:]\s*["']?true|` +
157
+ String.raw `enforceUserIsolation["']?\s*[=:]\s*["']?false|` +
158
+ String.raw `enableExportNotebook["']?\s*[=:]\s*["']?true`;
159
+ // 29. Audit / verbose logging disabled (per-line).
160
+ const DBX_AUDIT_LOGGING_PATTERN = String.raw `log_delivery[^=]*status\s*=\s*["']?DISABLED|` +
161
+ String.raw `status\s*=\s*["']DISABLED["']|` +
162
+ String.raw `spark\.databricks\.audit\.enabled["']?\s*[=:]\s*["']?false|` +
163
+ String.raw `enableVerboseAuditLogs["']?\s*[=:]\s*["']?false`;
164
+ // 30. CREATE FUNCTION external / Python from untrusted source.
165
+ const DBX_UNTRUSTED_FUNCTION_PATTERN = String.raw `CREATE\s+(?:OR\sREPLACE\s)?FUNCTION\b[^;]*LANGUAGE\s+PYTHON|` +
166
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?FUNCTION\b[^;]*USING\s+JAR\s+["']?(?:dbfs:/|s3a?://|https?://)|` +
167
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?FUNCTION\b[^;]*AS\s+["'][^"']*\b(?:os\.system|subprocess|eval)\b`;
168
+ // 31. No cluster policy = unrestricted cluster creation.
169
+ const DBX_CLUSTER_POLICY_PRESENT_PATTERN = String.raw `databricks_cluster_policy|policy_id\s*=`;
170
+ const DBX_CLUSTER_PRESENT_PATTERN = String.raw `resource\s+"databricks_cluster"|resource\s+"databricks_job"`;
171
+ // ---------------------------------------------------------------------------
172
+ // Snowflake DEPTH patterns (Round 2)
173
+ // ---------------------------------------------------------------------------
174
+ // 32. OAuth security integration with wildcard/http redirect or missing blocked-roles.
175
+ const SF_OAUTH_INTEGRATION_PATTERN = String.raw `OAUTH_REDIRECT_URI\s*=\s*["']http://|` +
176
+ String.raw `OAUTH_REDIRECT_URI\s*=\s*["'][^"']*\*|` +
177
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?SECURITY\s+INTEGRATION\b[^;]*TYPE\s*=\s*OAUTH|` +
178
+ String.raw `BLOCKED_ROLES_LIST\s*=\s*\(\s*\)`;
179
+ // 33. SCIM integration with no network policy reference.
180
+ const SF_SCIM_INTEGRATION_PATTERN = String.raw `SECURITY\s+INTEGRATION\b[^;]*TYPE\s*=\s*SCIM|` +
181
+ String.raw `SCIM_CLIENT\s*=|` +
182
+ String.raw `snowflake_scim_integration\b`;
183
+ // A securely-configured SCIM integration references a NETWORK_POLICY on its definition.
184
+ const SF_SCIM_HAS_NETWORK_POLICY_PATTERN = String.raw `SCIM[^;]*NETWORK_POLICY\s*=|network_policy\s*=[^;]*scim`;
185
+ // 34. Storage integration allowing all (*) locations.
186
+ const SF_STORAGE_INTEGRATION_WILD_PATTERN = String.raw `STORAGE_ALLOWED_LOCATIONS\s*=\s*\(\s*["']\*["']|` +
187
+ String.raw `STORAGE_ALLOWED_LOCATIONS\s*=\s*\(\s*["'](?:s3|gcs|azure)://["']|` +
188
+ String.raw `storage_allowed_locations\s*=\s*\[\s*["']\*`;
189
+ // 35. External function / API integration to untrusted endpoint.
190
+ const SF_EXTERNAL_FUNCTION_PATTERN = String.raw `CREATE\s+(?:OR\sREPLACE\s)?EXTERNAL\s+FUNCTION\b|` +
191
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?API\s+INTEGRATION\b|` +
192
+ String.raw `API_ALLOWED_PREFIXES\s*=\s*\(\s*["']https?://[^"']*\*|` +
193
+ String.raw `API_ALLOWED_PREFIXES\s*=\s*\(\s*["']http://`;
194
+ // 36. Tasks / streams owned by ACCOUNTADMIN.
195
+ const SF_TASK_STREAM_ADMIN_PATTERN = String.raw `GRANT\s+OWNERSHIP\s+ON\s+TASK\b[^;]*\bTO\s+(?:ROLE\s)?ACCOUNTADMIN|` +
196
+ String.raw `GRANT\s+OWNERSHIP\s+ON\s+STREAM\b[^;]*\bTO\s+(?:ROLE\s)?ACCOUNTADMIN|` +
197
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?TASK\b[^;]*USER_TASK_MANAGED_INITIAL_WAREHOUSE_SIZE[^;]*\bSCHEDULE\b`;
198
+ // 37. Broad privilege grants: IMPORTED PRIVILEGES / MANAGE GRANTS / APPLY MASKING POLICY.
199
+ const SF_BROAD_PRIV_GRANT_PATTERN = String.raw `GRANT\s+IMPORTED\s+PRIVILEGES\b|` +
200
+ String.raw `GRANT\s+MANAGE\s+GRANTS\b[^;]*\bTO\b|` +
201
+ String.raw `GRANT\s+APPLY\s+MASKING\s+POLICY\b[^;]*\bTO\s+(?:ROLE\s)?(?:PUBLIC|[A-Z_]*USER)|` +
202
+ String.raw `GRANT\s+APPLY\s+(?:ROW\s+ACCESS\s+POLICY|TAG)\b[^;]*\bTO\s+(?:ROLE\s)?PUBLIC`;
203
+ // 38. Stored procedure EXECUTE AS OWNER (privilege escalation via SQL injection).
204
+ const SF_PROC_EXECUTE_AS_OWNER_PATTERN = String.raw `CREATE\s+(?:OR\sREPLACE\s)?PROCEDURE\b[^$;]*EXECUTE\s+AS\s+OWNER|` +
205
+ String.raw `EXECUTE\s+AS\s+OWNER\b`;
206
+ // 39. DEFAULT_SECONDARY_ROLES = ('ALL') — all roles active on login.
207
+ const SF_SECONDARY_ROLES_ALL_PATTERN = String.raw `DEFAULT_SECONDARY_ROLES\s*=\s*\(\s*["']ALL["']\s*\)|` +
208
+ String.raw `default_secondary_roles\s*=\s*\[\s*["']ALL["']`;
209
+ // 40. Pipe / stage with inline cloud credentials.
210
+ const SF_PIPE_STAGE_CRED_PATTERN = String.raw `CREATE\s+(?:OR\sREPLACE\s)?PIPE\b[^;]*CREDENTIALS\s*=|` +
211
+ String.raw `AZURE_SAS_TOKEN\s*=\s*["'][^"']+["']|` +
212
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?STAGE\b[^;]*AZURE_SAS_TOKEN|` +
213
+ String.raw `AWS_TOKEN\s*=\s*["'][^"']+["']`;
214
+ // 41. Share to wildcard / public listing.
215
+ const SF_SHARE_WILDCARD_PATTERN = String.raw `ALTER\s+SHARE\s+[^;]*ADD\s+ACCOUNTS\s*=\s*["']?\*|` +
216
+ String.raw `CREATE\s+(?:OR\sREPLACE\s)?LISTING\b[^;]*\bPUBLIC\b|` +
217
+ String.raw `SET\s+ACCOUNTS\s*=\s*\(\s*["']\*["']`;
218
+ // 42. Password policy present (absence check) + lockout/min-length absence helpers.
219
+ const SF_PASSWORD_POLICY_PRESENT_PATTERN = String.raw `CREATE\s+(?:OR\sREPLACE\s)?PASSWORD\s+POLICY|snowflake_password_policy|PASSWORD_MIN_LENGTH`;
220
+ // 43. ACCESS_HISTORY / LOGIN_HISTORY usage (absence heuristic, LOW).
221
+ const SF_HISTORY_USAGE_PATTERN = String.raw `ACCOUNT_USAGE\.(?:ACCESS_HISTORY|LOGIN_HISTORY)|INFORMATION_SCHEMA\.LOGIN_HISTORY`;
222
+ // 44. Warehouse AUTO_SUSPEND disabled (cost / runaway compute, LOW).
223
+ const SF_WAREHOUSE_NO_SUSPEND_PATTERN = String.raw `CREATE\s+(?:OR\sREPLACE\s)?WAREHOUSE\b[^;]*AUTO_SUSPEND\s*=\s*0|` +
224
+ String.raw `AUTO_SUSPEND\s*=\s*0\b|` +
225
+ String.raw `auto_suspend\s*=\s*0\b`;
226
+ // 45. Time Travel disabled on (sensitive) tables: DATA_RETENTION_TIME_IN_DAYS = 0.
227
+ const SF_DATA_RETENTION_ZERO_PATTERN = String.raw `DATA_RETENTION_TIME_IN_DAYS\s*=\s*0\b|` +
228
+ String.raw `data_retention_time_in_days\s*=\s*0\b`;
229
+ // 46. PERIODIC_DATA_REKEYING disabled.
230
+ const SF_REKEYING_OFF_PATTERN = String.raw `PERIODIC_DATA_REKEYING\s*=\s*FALSE|periodic_data_rekeying\s*=\s*false`;
231
+ function ev(matches) {
232
+ return matches.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`);
233
+ }
234
+ export async function checkDataPlatform(_opts) {
235
+ const findings = [];
236
+ const [dbxToken, dbxSecretLeak, dbxWeakIso, dbxInit, dbxPublic, dbxTokenRes, dbxInlineCreds, dbxLegacy, sfGrant, sfUserPw, sfWeakAuth, sfNetOpen, sfNetPresent, sfUsage, sfConn, sfShare, sfPii, sfMasking, sfWeakenAccount, dbxUcBroadGrant, dbxExtLocation, dbxServerlessNoAcl, dbxSparkConfKey, dbxSingleUserMismatch, dbxModelServingPublic, dbxPermCanManage, dbxGitCred, dbxJobRunAs, dbxDbfsMountKey, dbxInstanceProfile, dbxWorkspaceConf, dbxAuditLogging, dbxUntrustedFunc, dbxClusterPolicyPresent, dbxClusterPresent, sfOauthInteg, sfScimInteg, sfScimHasNetPolicy, sfStorageWild, sfExtFunc, sfTaskStreamAdmin, sfBroadPriv, sfProcExecOwner, sfSecondaryRolesAll, sfPipeStageCred, sfShareWildcard, sfPwPolicyPresent, sfHistoryUsage, sfWhNoSuspend, sfDataRetentionZero, sfRekeyingOff,] = await Promise.all([
237
+ searchRepo({ query: DBX_HARDCODED_TOKEN_PATTERN, isRegex: true, maxMatches: 200 }),
238
+ searchRepo({ query: DBX_SECRET_LEAK_PATTERN, isRegex: true, maxMatches: 200 }),
239
+ searchRepo({ query: DBX_WEAK_ISOLATION_PATTERN, isRegex: true, maxMatches: 200 }),
240
+ searchRepo({ query: DBX_INIT_SCRIPT_PATTERN, isRegex: true, maxMatches: 200 }),
241
+ searchRepo({ query: DBX_PUBLIC_NETWORK_PATTERN, isRegex: true, maxMatches: 200 }),
242
+ searchRepo({ query: DBX_TOKEN_RESOURCE_PATTERN, isRegex: true, maxMatches: 200 }),
243
+ searchRepo({ query: DBX_INLINE_CREDS_PATTERN, isRegex: true, maxMatches: 200 }),
244
+ searchRepo({ query: DBX_LEGACY_METASTORE_PATTERN, isRegex: true, maxMatches: 200 }),
245
+ searchRepo({ query: SF_OVERPRIV_GRANT_PATTERN, isRegex: true, maxMatches: 200 }),
246
+ searchRepo({ query: SF_USER_PASSWORD_PATTERN, isRegex: true, maxMatches: 200 }),
247
+ searchRepo({ query: SF_WEAK_AUTH_PATTERN, isRegex: true, maxMatches: 200 }),
248
+ searchRepo({ query: SF_NETWORK_OPEN_PATTERN, isRegex: true, maxMatches: 200 }),
249
+ searchRepo({ query: SF_NETWORK_POLICY_PRESENT_PATTERN, isRegex: true, maxMatches: 5 }),
250
+ searchRepo({ query: SF_USAGE_PATTERN, isRegex: true, maxMatches: 5 }),
251
+ searchRepo({ query: SF_HARDCODED_CONN_PATTERN, isRegex: true, maxMatches: 200 }),
252
+ searchRepo({ query: SF_SHARE_STAGE_PATTERN, isRegex: true, maxMatches: 200 }),
253
+ searchRepo({ query: SF_PII_COLUMN_PATTERN, isRegex: true, maxMatches: 200 }),
254
+ searchRepo({ query: SF_MASKING_PRESENT_PATTERN, isRegex: true, maxMatches: 5 }),
255
+ searchRepo({ query: SF_WEAKEN_ACCOUNT_PATTERN, isRegex: true, maxMatches: 200 }),
256
+ searchRepo({ query: DBX_UC_BROAD_GRANT_PATTERN, isRegex: true, maxMatches: 200 }),
257
+ searchRepo({ query: DBX_EXTERNAL_LOCATION_PATTERN, isRegex: true, maxMatches: 200 }),
258
+ searchRepo({ query: DBX_SERVERLESS_NO_ACL_PATTERN, isRegex: true, maxMatches: 200 }),
259
+ searchRepo({ query: DBX_SPARK_CONF_KEY_PATTERN, isRegex: true, maxMatches: 200 }),
260
+ searchRepo({ query: DBX_SINGLE_USER_MISMATCH_PATTERN, isRegex: true, maxMatches: 200 }),
261
+ searchRepo({ query: DBX_MODEL_SERVING_PUBLIC_PATTERN, isRegex: true, maxMatches: 200 }),
262
+ searchRepo({ query: DBX_PERMISSIONS_CAN_MANAGE_PATTERN, isRegex: true, maxMatches: 200 }),
263
+ searchRepo({ query: DBX_GIT_CREDENTIAL_PATTERN, isRegex: true, maxMatches: 200 }),
264
+ searchRepo({ query: DBX_JOB_RUN_AS_PATTERN, isRegex: true, maxMatches: 200 }),
265
+ searchRepo({ query: DBX_DBFS_MOUNT_KEY_PATTERN, isRegex: true, maxMatches: 200 }),
266
+ searchRepo({ query: DBX_INSTANCE_PROFILE_PATTERN, isRegex: true, maxMatches: 200 }),
267
+ searchRepo({ query: DBX_WORKSPACE_CONF_PATTERN, isRegex: true, maxMatches: 200 }),
268
+ searchRepo({ query: DBX_AUDIT_LOGGING_PATTERN, isRegex: true, maxMatches: 200 }),
269
+ searchRepo({ query: DBX_UNTRUSTED_FUNCTION_PATTERN, isRegex: true, maxMatches: 200 }),
270
+ searchRepo({ query: DBX_CLUSTER_POLICY_PRESENT_PATTERN, isRegex: true, maxMatches: 5 }),
271
+ searchRepo({ query: DBX_CLUSTER_PRESENT_PATTERN, isRegex: true, maxMatches: 5 }),
272
+ searchRepo({ query: SF_OAUTH_INTEGRATION_PATTERN, isRegex: true, maxMatches: 200 }),
273
+ searchRepo({ query: SF_SCIM_INTEGRATION_PATTERN, isRegex: true, maxMatches: 200 }),
274
+ searchRepo({ query: SF_SCIM_HAS_NETWORK_POLICY_PATTERN, isRegex: true, maxMatches: 5 }),
275
+ searchRepo({ query: SF_STORAGE_INTEGRATION_WILD_PATTERN, isRegex: true, maxMatches: 200 }),
276
+ searchRepo({ query: SF_EXTERNAL_FUNCTION_PATTERN, isRegex: true, maxMatches: 200 }),
277
+ searchRepo({ query: SF_TASK_STREAM_ADMIN_PATTERN, isRegex: true, maxMatches: 200 }),
278
+ searchRepo({ query: SF_BROAD_PRIV_GRANT_PATTERN, isRegex: true, maxMatches: 200 }),
279
+ searchRepo({ query: SF_PROC_EXECUTE_AS_OWNER_PATTERN, isRegex: true, maxMatches: 200 }),
280
+ searchRepo({ query: SF_SECONDARY_ROLES_ALL_PATTERN, isRegex: true, maxMatches: 200 }),
281
+ searchRepo({ query: SF_PIPE_STAGE_CRED_PATTERN, isRegex: true, maxMatches: 200 }),
282
+ searchRepo({ query: SF_SHARE_WILDCARD_PATTERN, isRegex: true, maxMatches: 200 }),
283
+ searchRepo({ query: SF_PASSWORD_POLICY_PRESENT_PATTERN, isRegex: true, maxMatches: 5 }),
284
+ searchRepo({ query: SF_HISTORY_USAGE_PATTERN, isRegex: true, maxMatches: 5 }),
285
+ searchRepo({ query: SF_WAREHOUSE_NO_SUSPEND_PATTERN, isRegex: true, maxMatches: 200 }),
286
+ searchRepo({ query: SF_DATA_RETENTION_ZERO_PATTERN, isRegex: true, maxMatches: 200 }),
287
+ searchRepo({ query: SF_REKEYING_OFF_PATTERN, isRegex: true, maxMatches: 200 }),
288
+ ]);
289
+ // 1.
290
+ if (dbxToken.length > 0) {
291
+ findings.push({
292
+ id: "DATABRICKS_HARDCODED_TOKEN",
293
+ title: "Hardcoded Databricks PAT or host URL with embedded credentials",
294
+ severity: "CRITICAL",
295
+ evidence: ev(dbxToken),
296
+ requiredActions: [
297
+ "Remove the dapi… personal access token from source and revoke it in the Databricks user settings immediately.",
298
+ "Inject the token at runtime from a secret scope (databricks secrets) or the DATABRICKS_TOKEN env supplied by a secret manager.",
299
+ "Use OAuth (U2M/M2M) or service-principal credentials instead of long-lived PATs.",
300
+ "Strip embedded user:password from any workspace host URL.",
301
+ ],
302
+ });
303
+ }
304
+ // 2.
305
+ if (dbxSecretLeak.length > 0) {
306
+ findings.push({
307
+ id: "DATABRICKS_SECRET_LEAK",
308
+ title: "Databricks secret value printed/logged after dbutils.secrets.get",
309
+ severity: "HIGH",
310
+ evidence: ev(dbxSecretLeak),
311
+ requiredActions: [
312
+ "Never print, log, displayHTML, or echo the result of dbutils.secrets.get — Databricks only redacts notebook cell output, not logs.",
313
+ "Pass the secret directly into the consuming API call; do not assign it to a printed variable.",
314
+ "Audit cluster logs and notebook revision history for any leaked secret and rotate it.",
315
+ ],
316
+ });
317
+ }
318
+ // 3.
319
+ if (dbxWeakIso.length > 0) {
320
+ findings.push({
321
+ id: "DATABRICKS_WEAK_CLUSTER_ISOLATION",
322
+ title: "Databricks cluster lacks Unity Catalog isolation / table ACLs disabled",
323
+ severity: "HIGH",
324
+ evidence: ev(dbxWeakIso),
325
+ requiredActions: [
326
+ "Set data_security_mode = \"USER_ISOLATION\" (or \"SINGLE_USER\" for ML) instead of NONE/LEGACY_* on all clusters.",
327
+ "Enable table ACLs: spark.databricks.acl.dfAclsEnabled true and sqlOnly enforcement.",
328
+ "Migrate to Unity Catalog so access is governed centrally rather than per-cluster.",
329
+ ],
330
+ });
331
+ }
332
+ // 4.
333
+ if (dbxInit.length > 0) {
334
+ findings.push({
335
+ id: "DATABRICKS_INIT_SCRIPT_UNTRUSTED",
336
+ title: "Cluster init script sourced from DBFS / external URL (tampering & supply-chain risk)",
337
+ severity: "HIGH",
338
+ evidence: ev(dbxInit),
339
+ requiredActions: [
340
+ "Store init scripts in a Unity Catalog volume or workspace files, not in world-writable dbfs:/ paths.",
341
+ "Avoid global init scripts that fetch from external http(s) URLs — they run as root on every cluster.",
342
+ "Pin and checksum any externally sourced script and host it in a controlled, access-restricted location.",
343
+ ],
344
+ });
345
+ }
346
+ // 5.
347
+ if (dbxPublic.length > 0) {
348
+ findings.push({
349
+ id: "DATABRICKS_PUBLIC_NETWORK",
350
+ title: "Databricks cluster/workspace exposed to the public internet",
351
+ severity: "HIGH",
352
+ evidence: ev(dbxPublic),
353
+ requiredActions: [
354
+ "Set enable_no_public_ip = true (Secure Cluster Connectivity / no-public-IP) for the workspace.",
355
+ "Attach a databricks_ip_access_list with enabled = true restricting access to corporate CIDRs.",
356
+ "Deploy the workspace into a customer-managed VPC/VNet with Private Link / Private Endpoint.",
357
+ ],
358
+ });
359
+ }
360
+ // 6.
361
+ if (dbxTokenRes.length > 0) {
362
+ findings.push({
363
+ id: "DATABRICKS_TOKEN_RESOURCE_LONG_LIVED",
364
+ title: "databricks_token resource with no/long expiry or admin service principal",
365
+ severity: "MEDIUM",
366
+ evidence: ev(dbxTokenRes),
367
+ requiredActions: [
368
+ "Set a short lifetime_seconds (e.g. <= 3600) and rotate tokens automatically; never use -1 (no expiry).",
369
+ "Scope service principals to least privilege — remove allow_cluster_create/admin unless strictly required.",
370
+ "Prefer OAuth M2M tokens over static databricks_token resources for automation.",
371
+ ],
372
+ });
373
+ }
374
+ // 7.
375
+ if (dbxInlineCreds.length > 0) {
376
+ findings.push({
377
+ id: "DATABRICKS_INLINE_CREDENTIALS",
378
+ title: "Inline storage credentials/keys in spark.conf.set or DataFrame .option()",
379
+ severity: "CRITICAL",
380
+ evidence: ev(dbxInlineCreds),
381
+ requiredActions: [
382
+ "Remove inline fs.s3a / fs.azure.account.key / JDBC user+password literals and rotate the exposed keys.",
383
+ "Use Unity Catalog external locations + storage credentials, or instance profiles / managed identities.",
384
+ "Reference secrets via dbutils.secrets.get from a backed secret scope instead of literals.",
385
+ ],
386
+ });
387
+ }
388
+ // 8.
389
+ if (dbxLegacy.length > 0) {
390
+ findings.push({
391
+ id: "DATABRICKS_LEGACY_HIVE_METASTORE",
392
+ title: "Use of legacy hive_metastore / Unity Catalog disabled (no central governance)",
393
+ severity: "MEDIUM",
394
+ evidence: ev(dbxLegacy),
395
+ requiredActions: [
396
+ "Create and assign a Unity Catalog metastore to the workspace and migrate tables off hive_metastore.",
397
+ "Do not disable spark.databricks.unityCatalog.enabled.",
398
+ "Govern table/column access and lineage through Unity Catalog rather than the legacy Hive metastore.",
399
+ ],
400
+ });
401
+ }
402
+ // 9.
403
+ if (sfGrant.length > 0) {
404
+ findings.push({
405
+ id: "SNOWFLAKE_OVERPRIVILEGED_GRANT",
406
+ title: "Over-privileged Snowflake grant (ACCOUNTADMIN/SECURITYADMIN, ALL PRIVILEGES, or TO PUBLIC)",
407
+ severity: "HIGH",
408
+ evidence: ev(sfGrant),
409
+ requiredActions: [
410
+ "Never grant ACCOUNTADMIN/SECURITYADMIN to functional or service roles — limit to a small set of named humans with MFA.",
411
+ "Replace GRANT ALL PRIVILEGES with explicit, least-privilege grants on specific objects.",
412
+ "Never GRANT … TO PUBLIC; PUBLIC is inherited by every user in the account.",
413
+ "Build a role hierarchy with custom functional roles owned by SYSADMIN.",
414
+ ],
415
+ });
416
+ }
417
+ // 10.
418
+ if (sfUserPw.length > 0) {
419
+ findings.push({
420
+ id: "SNOWFLAKE_HARDCODED_USER_PASSWORD",
421
+ title: "CREATE USER with hardcoded password or MUST_CHANGE_PASSWORD=FALSE",
422
+ severity: "CRITICAL",
423
+ evidence: ev(sfUserPw),
424
+ requiredActions: [
425
+ "Remove the literal PASSWORD = '…' from SQL/Terraform and rotate the credential.",
426
+ "Set MUST_CHANGE_PASSWORD = TRUE for any human user, or omit password entirely and use key-pair/SSO.",
427
+ "Manage Snowflake user secrets via a secret manager + Terraform sensitive variables, never inline.",
428
+ ],
429
+ });
430
+ }
431
+ // 11.
432
+ if (sfWeakAuth.length > 0) {
433
+ findings.push({
434
+ id: "SNOWFLAKE_WEAK_AUTH",
435
+ title: "Weak Snowflake authentication (session keepalive / MFA bypass, no key-pair auth)",
436
+ severity: "HIGH",
437
+ evidence: ev(sfWeakAuth),
438
+ requiredActions: [
439
+ "Do not set ALLOW_CLIENT_SET_SESSION_KEEPALIVE / CLIENT_SESSION_KEEP_ALIVE = TRUE — it extends sessions past idle timeout.",
440
+ "Enforce MFA for all human users and do not configure MINS_TO_BYPASS_MFA.",
441
+ "Use RSA key-pair authentication (RSA_PUBLIC_KEY) for service accounts instead of passwords.",
442
+ ],
443
+ });
444
+ }
445
+ // 12.
446
+ if (sfNetOpen.length > 0) {
447
+ findings.push({
448
+ id: "SNOWFLAKE_NETWORK_POLICY_OPEN",
449
+ title: "Snowflake network policy allows all IPs (0.0.0.0/0 or *)",
450
+ severity: "HIGH",
451
+ evidence: ev(sfNetOpen),
452
+ requiredActions: [
453
+ "Restrict ALLOWED_IP_LIST to specific corporate/VPN CIDR ranges; never use 0.0.0.0/0 or '*'.",
454
+ "Apply the network policy at the account level and to privileged users.",
455
+ "Prefer Snowflake Private Link / private connectivity over public IP allowlisting.",
456
+ ],
457
+ });
458
+ // ABSENCE BRANCH (intentionally not exercised by fixtures): SNOWFLAKE_NO_NETWORK_POLICY
459
+ // fires only when Snowflake is in use AND the repo defines NO network policy at all. The
460
+ // insecure fixtures deliberately include an *open* (0.0.0.0/0) network policy to exercise
461
+ // the SNOWFLAKE_NETWORK_POLICY_OPEN branch above, so this complementary branch is mutually
462
+ // exclusive with that state and stays dormant here. It is reachable in any Snowflake repo
463
+ // that has zero CREATE NETWORK POLICY / snowflake_network_policy declarations.
464
+ }
465
+ else if (sfUsage.length > 0 && sfNetPresent.length === 0) {
466
+ findings.push({
467
+ id: "SNOWFLAKE_NO_NETWORK_POLICY",
468
+ title: "Snowflake in use but no network policy defined — account reachable from any IP",
469
+ severity: "MEDIUM",
470
+ requiredActions: [
471
+ "Create a network policy (CREATE NETWORK POLICY / snowflake_network_policy) with an explicit ALLOWED_IP_LIST.",
472
+ "Attach the policy at the account level via ALTER ACCOUNT SET NETWORK_POLICY.",
473
+ "Use Private Link for VPC-internal connectivity to Snowflake.",
474
+ ],
475
+ });
476
+ }
477
+ // 13.
478
+ if (sfConn.length > 0) {
479
+ findings.push({
480
+ id: "SNOWFLAKE_HARDCODED_CONNECTION",
481
+ title: "Hardcoded Snowflake connection credentials in code or Terraform",
482
+ severity: "CRITICAL",
483
+ evidence: ev(sfConn),
484
+ requiredActions: [
485
+ "Remove hardcoded account/password literals and rotate the credentials.",
486
+ "Source Snowflake credentials from a secret manager (AWS Secrets Manager, Vault, etc.) at runtime.",
487
+ "In Terraform, use sensitive variables and a secrets backend; never commit account/password values.",
488
+ ],
489
+ });
490
+ }
491
+ // 14.
492
+ if (sfShare.length > 0) {
493
+ findings.push({
494
+ id: "SNOWFLAKE_DATA_SHARE_OR_EXTERNAL_STAGE",
495
+ title: "Snowflake data share to accounts or external stage with hardcoded cloud credentials",
496
+ severity: "HIGH",
497
+ evidence: ev(sfShare),
498
+ requiredActions: [
499
+ "Review every CREATE SHARE / ALTER SHARE … ADD ACCOUNTS — share only the minimum objects with named, expected accounts.",
500
+ "Do not embed AWS_KEY_ID/AWS_SECRET_KEY in CREATE STAGE CREDENTIALS; use a STORAGE INTEGRATION instead.",
501
+ "Rotate any cloud keys that were committed and enforce REQUIRE_STORAGE_INTEGRATION_FOR_STAGE_CREATION = TRUE.",
502
+ ],
503
+ });
504
+ }
505
+ // 15.
506
+ if (sfPii.length > 0 && sfMasking.length === 0) {
507
+ findings.push({
508
+ id: "SNOWFLAKE_PII_NO_MASKING_POLICY",
509
+ title: "PII-shaped columns defined without any masking or row-access policy",
510
+ severity: "LOW",
511
+ evidence: ev(sfPii),
512
+ requiredActions: [
513
+ "Apply a Snowflake MASKING POLICY to columns containing PII (SSN, card number, email, DOB, etc.).",
514
+ "Use ROW ACCESS POLICY to restrict row visibility by role where appropriate.",
515
+ "Tag PII columns with object tags and enforce tag-based masking centrally.",
516
+ ],
517
+ });
518
+ }
519
+ // 16.
520
+ if (sfWeakenAccount.length > 0) {
521
+ findings.push({
522
+ id: "SNOWFLAKE_WEAKENED_ACCOUNT_PARAM",
523
+ title: "ALTER ACCOUNT setting a security parameter to FALSE (security downgrade)",
524
+ severity: "HIGH",
525
+ evidence: ev(sfWeakenAccount),
526
+ requiredActions: [
527
+ "Keep REQUIRE_STORAGE_INTEGRATION_FOR_STAGE_CREATION = TRUE so stages cannot embed raw cloud keys.",
528
+ "Keep PREVENT_UNLOAD_TO_INLINE_URL = TRUE to block data exfiltration via ad-hoc URLs.",
529
+ "Review every ALTER ACCOUNT SET … = FALSE and justify or revert any security parameter downgrade.",
530
+ ],
531
+ });
532
+ }
533
+ // 17.
534
+ if (dbxUcBroadGrant.length > 0) {
535
+ findings.push({
536
+ id: "DATABRICKS_UC_BROAD_GRANT",
537
+ title: "Unity Catalog ALL PRIVILEGES / MANAGE granted to the whole-account users group",
538
+ severity: "HIGH",
539
+ evidence: ev(dbxUcBroadGrant),
540
+ requiredActions: [
541
+ "Never GRANT ALL PRIVILEGES or MANAGE on a catalog/schema to `account users` or the `users` group.",
542
+ "Grant least-privilege (USE CATALOG, USE SCHEMA, SELECT) to specific functional groups instead.",
543
+ "Reserve MANAGE/OWNERSHIP for a small data-governance group, not all account users.",
544
+ ],
545
+ });
546
+ }
547
+ // 18.
548
+ if (dbxExtLocation.length > 0) {
549
+ findings.push({
550
+ id: "DATABRICKS_EXTERNAL_LOCATION_BROAD",
551
+ title: "Unity Catalog external location / storage credential is public or over-broad",
552
+ severity: "HIGH",
553
+ evidence: ev(dbxExtLocation),
554
+ requiredActions: [
555
+ "Scope external location URLs to exact prefixes — never use wildcard (*) storage paths.",
556
+ "Do not set skip_validation = true; validate the storage credential against the bucket.",
557
+ "Grant READ/WRITE FILES on external locations to specific roles, not `account users`/`users`.",
558
+ ],
559
+ });
560
+ }
561
+ // 19.
562
+ if (dbxServerlessNoAcl.length > 0) {
563
+ findings.push({
564
+ id: "DATABRICKS_SERVERLESS_NO_IP_ACL",
565
+ title: "Serverless SQL warehouse enabled with no IP access list restriction",
566
+ severity: "HIGH",
567
+ evidence: ev(dbxServerlessNoAcl),
568
+ requiredActions: [
569
+ "Attach a databricks_ip_access_list (enabled = true) restricting serverless SQL access to corporate CIDRs.",
570
+ "Enable serverless egress controls / network connectivity config (NCC) for the workspace.",
571
+ "Disable serverless compute if Private Link-only connectivity is required.",
572
+ ],
573
+ });
574
+ }
575
+ // 20.
576
+ if (dbxSparkConfKey.length > 0) {
577
+ findings.push({
578
+ id: "DATABRICKS_SPARK_CONF_KEY_INLINE",
579
+ title: "Cluster spark_conf exposes a storage account/access key inline",
580
+ severity: "CRITICAL",
581
+ evidence: ev(dbxSparkConfKey),
582
+ requiredActions: [
583
+ "Remove fs.azure.account.key / fs.s3a.*.key literals from spark_conf and rotate the exposed keys.",
584
+ "Reference secrets via {{secrets/scope/key}} spark_conf syntax backed by a Databricks secret scope.",
585
+ "Prefer Unity Catalog storage credentials, instance profiles, or managed identities over inline keys.",
586
+ ],
587
+ });
588
+ }
589
+ // 21.
590
+ if (dbxSingleUserMismatch.length > 0) {
591
+ findings.push({
592
+ id: "DATABRICKS_SINGLE_USER_ISOLATION_MISMATCH",
593
+ title: "Cluster single_user_name / data_security_mode mismatch weakens isolation",
594
+ severity: "MEDIUM",
595
+ evidence: ev(dbxSingleUserMismatch),
596
+ requiredActions: [
597
+ "For SINGLE_USER mode set a real single_user_name; do not leave it empty.",
598
+ "Do not pair single_user_name with NONE security mode — it provides no Unity Catalog isolation.",
599
+ "Use USER_ISOLATION (shared) clusters for multi-user workloads.",
600
+ ],
601
+ });
602
+ }
603
+ // 22.
604
+ if (dbxModelServingPublic.length > 0) {
605
+ findings.push({
606
+ id: "DATABRICKS_MODEL_SERVING_PUBLIC",
607
+ title: "Model serving endpoint is public / queryable by all users with no auth",
608
+ severity: "HIGH",
609
+ evidence: ev(dbxModelServingPublic),
610
+ requiredActions: [
611
+ "Require authentication (PAT/OAuth) on all model serving endpoints; never expose them with auth=none.",
612
+ "Grant CAN_QUERY to specific service principals/groups, not the `users` group.",
613
+ "Front public inference with an authenticated API gateway and rate limiting.",
614
+ ],
615
+ });
616
+ }
617
+ // 23.
618
+ if (dbxPermCanManage.length > 0) {
619
+ findings.push({
620
+ id: "DATABRICKS_PERMISSIONS_CAN_MANAGE_USERS",
621
+ title: "databricks_permissions grants CAN_MANAGE to the all-users group",
622
+ severity: "HIGH",
623
+ evidence: ev(dbxPermCanManage),
624
+ requiredActions: [
625
+ "Never grant CAN_MANAGE on jobs/clusters/pipelines to the `users` or `account users` group.",
626
+ "Grant CAN_VIEW or CAN_RUN to broad groups; reserve CAN_MANAGE for named owners/admins.",
627
+ "Audit object ACLs and remove broad management grants.",
628
+ ],
629
+ });
630
+ }
631
+ // 24.
632
+ if (dbxGitCred.length > 0) {
633
+ findings.push({
634
+ id: "DATABRICKS_GIT_CREDENTIAL_INLINE_PAT",
635
+ title: "Databricks Repos git credential contains an inline personal access token",
636
+ severity: "CRITICAL",
637
+ evidence: ev(dbxGitCred),
638
+ requiredActions: [
639
+ "Remove the inline git PAT (ghp_/glpat-/dapi…) from databricks_git_credential and revoke it.",
640
+ "Provide the PAT via a Terraform sensitive variable sourced from a secret manager.",
641
+ "Prefer fine-grained, expiring git tokens or app-based git integration over long-lived PATs.",
642
+ ],
643
+ });
644
+ }
645
+ // 25.
646
+ if (dbxJobRunAs.length > 0) {
647
+ findings.push({
648
+ id: "DATABRICKS_JOB_RUN_AS_ELEVATED",
649
+ title: "Job run_as uses an elevated service principal / admin identity",
650
+ severity: "MEDIUM",
651
+ evidence: ev(dbxJobRunAs),
652
+ requiredActions: [
653
+ "Run jobs as a least-privilege service principal scoped only to the catalogs/schemas the job needs.",
654
+ "Avoid run_as identities named *admin or with workspace-admin entitlements.",
655
+ "Review run_as_owner = true jobs — they execute with the owner's full privileges.",
656
+ ],
657
+ });
658
+ }
659
+ // 26.
660
+ if (dbxDbfsMountKey.length > 0) {
661
+ findings.push({
662
+ id: "DATABRICKS_DBFS_MOUNT_INLINE_KEY",
663
+ title: "DBFS mount configured with an inline storage account/access key",
664
+ severity: "CRITICAL",
665
+ evidence: ev(dbxDbfsMountKey),
666
+ requiredActions: [
667
+ "Remove storage keys from dbutils.fs.mount extra_configs and rotate the exposed credentials.",
668
+ "Reference the key via dbutils.secrets.get from a secret scope, or use a UC external location instead.",
669
+ "Migrate legacy DBFS mounts to Unity Catalog volumes with managed storage credentials.",
670
+ ],
671
+ });
672
+ }
673
+ // 27.
674
+ if (dbxInstanceProfile.length > 0) {
675
+ findings.push({
676
+ id: "DATABRICKS_INSTANCE_PROFILE_OVERPRIVILEGED",
677
+ title: "Cluster attached to an overprivileged instance profile / IAM role",
678
+ severity: "HIGH",
679
+ evidence: ev(dbxInstanceProfile),
680
+ requiredActions: [
681
+ "Attach least-privilege instance profiles — avoid roles named *Admin/PowerUser/*FullAccess.",
682
+ "Scope the underlying IAM role to the specific S3 buckets/KMS keys the cluster requires.",
683
+ "Never use wildcard (*) instance-profile ARNs.",
684
+ ],
685
+ });
686
+ }
687
+ // 28.
688
+ if (dbxWorkspaceConf.length > 0) {
689
+ findings.push({
690
+ id: "DATABRICKS_WORKSPACE_CONF_WEAK",
691
+ title: "Workspace configuration weakens controls (PAT/DBFS browser/export enabled, user isolation off)",
692
+ severity: "MEDIUM",
693
+ evidence: ev(dbxWorkspaceConf),
694
+ requiredActions: [
695
+ "Set enforceUserIsolation = true and disable enableTokensConfig where OAuth is available.",
696
+ "Disable enableDbfsFileBrowser and enableExportNotebook to limit data exfiltration paths.",
697
+ "Govern workspace conf via Terraform and review every override.",
698
+ ],
699
+ });
700
+ }
701
+ // 29.
702
+ if (dbxAuditLogging.length > 0) {
703
+ findings.push({
704
+ id: "DATABRICKS_AUDIT_LOGGING_DISABLED",
705
+ title: "Databricks audit / verbose logging disabled",
706
+ severity: "HIGH",
707
+ evidence: ev(dbxAuditLogging),
708
+ requiredActions: [
709
+ "Enable databricks_mws_log_delivery (audit + billable usage) with status = ENABLED.",
710
+ "Set enableVerboseAuditLogs = true so notebook/command actions are captured.",
711
+ "Ship audit logs to a tamper-evident store and alert on privileged actions.",
712
+ ],
713
+ });
714
+ }
715
+ // 30.
716
+ if (dbxUntrustedFunc.length > 0) {
717
+ findings.push({
718
+ id: "DATABRICKS_UNTRUSTED_FUNCTION",
719
+ title: "CREATE FUNCTION using Python / external JAR / shell from an untrusted source",
720
+ severity: "HIGH",
721
+ evidence: ev(dbxUntrustedFunc),
722
+ requiredActions: [
723
+ "Review Python/JAR UDFs — they execute arbitrary code; restrict who can CREATE FUNCTION.",
724
+ "Load JARs only from a controlled, checksummed Unity Catalog volume, not dbfs:/ or external URLs.",
725
+ "Forbid os.system/subprocess/eval inside UDF bodies and run on isolation-enforced clusters.",
726
+ ],
727
+ });
728
+ }
729
+ // 31. No cluster policy = unrestricted cluster creation.
730
+ if (dbxClusterPresent.length > 0 && dbxClusterPolicyPresent.length === 0) {
731
+ findings.push({
732
+ id: "DATABRICKS_NO_CLUSTER_POLICY",
733
+ title: "Clusters/jobs defined with no cluster policy — unrestricted cluster creation",
734
+ severity: "MEDIUM",
735
+ requiredActions: [
736
+ "Create a databricks_cluster_policy and reference it via policy_id on all clusters/jobs.",
737
+ "Pin data_security_mode, instance types, autotermination, and forbid spark_conf secrets in the policy.",
738
+ "Restrict CAN_USE on cluster policies to specific groups to control who can launch compute.",
739
+ ],
740
+ });
741
+ }
742
+ // 32.
743
+ if (sfOauthInteg.length > 0) {
744
+ findings.push({
745
+ id: "SNOWFLAKE_OAUTH_INTEGRATION_WEAK",
746
+ title: "OAuth security integration with http/wildcard redirect URI or empty BLOCKED_ROLES_LIST",
747
+ severity: "HIGH",
748
+ evidence: ev(sfOauthInteg),
749
+ requiredActions: [
750
+ "Use exact https OAUTH_REDIRECT_URI values; never http:// or wildcard URIs.",
751
+ "Keep ACCOUNTADMIN/SECURITYADMIN in BLOCKED_ROLES_LIST so OAuth tokens cannot assume them.",
752
+ "Set short OAUTH_REFRESH_TOKEN_VALIDITY and scope the integration to specific clients.",
753
+ ],
754
+ });
755
+ }
756
+ // 33. SCIM integration present — its bearer token must be restricted by a NETWORK_POLICY
757
+ // attached to the integration. searchRepo is per-line so we cannot scope a NETWORK_POLICY
758
+ // assignment to the SCIM resource block; surface it whenever a SCIM integration is defined
759
+ // so the reviewer confirms the token is IP-restricted.
760
+ if (sfScimInteg.length > 0 && sfScimHasNetPolicy.length === 0) {
761
+ findings.push({
762
+ id: "SNOWFLAKE_SCIM_NO_NETWORK_POLICY",
763
+ title: "SCIM security integration present but no network policy restricts the SCIM token",
764
+ severity: "HIGH",
765
+ evidence: ev(sfScimInteg),
766
+ requiredActions: [
767
+ "Attach a NETWORK_POLICY to the SCIM integration so the bearer token is usable only from the IdP IP ranges.",
768
+ "Rotate the SCIM access token regularly and store it in a secret manager.",
769
+ "Restrict the run_as / owner role of the SCIM integration to least privilege.",
770
+ ],
771
+ });
772
+ }
773
+ // 34.
774
+ if (sfStorageWild.length > 0) {
775
+ findings.push({
776
+ id: "SNOWFLAKE_STORAGE_INTEGRATION_WILDCARD",
777
+ title: "Storage integration allows all (*) or root storage locations",
778
+ severity: "HIGH",
779
+ evidence: ev(sfStorageWild),
780
+ requiredActions: [
781
+ "Set STORAGE_ALLOWED_LOCATIONS to exact bucket/prefix paths; never '*' or a bare bucket root.",
782
+ "Populate STORAGE_BLOCKED_LOCATIONS for sensitive prefixes.",
783
+ "Bind the integration to a least-privilege cloud role scoped to those exact locations.",
784
+ ],
785
+ });
786
+ }
787
+ // 35.
788
+ if (sfExtFunc.length > 0) {
789
+ findings.push({
790
+ id: "SNOWFLAKE_EXTERNAL_FUNCTION_UNTRUSTED",
791
+ title: "External function / API integration to an untrusted or wildcard endpoint",
792
+ severity: "HIGH",
793
+ evidence: ev(sfExtFunc),
794
+ requiredActions: [
795
+ "Restrict API_ALLOWED_PREFIXES to exact https endpoints; never http:// or wildcard prefixes.",
796
+ "Review external functions — they exfiltrate row data to the remote endpoint on every call.",
797
+ "Bind the API integration to a dedicated cloud role and enforce request signing.",
798
+ ],
799
+ });
800
+ }
801
+ // 36.
802
+ if (sfTaskStreamAdmin.length > 0) {
803
+ findings.push({
804
+ id: "SNOWFLAKE_TASK_STREAM_ADMIN_OWNED",
805
+ title: "Tasks/streams owned by ACCOUNTADMIN or running with elevated privilege",
806
+ severity: "MEDIUM",
807
+ evidence: ev(sfTaskStreamAdmin),
808
+ requiredActions: [
809
+ "Own tasks and streams with a least-privilege custom role, never ACCOUNTADMIN.",
810
+ "Tasks run with the privileges of their owning role — keep that role minimal.",
811
+ "Grant EXECUTE TASK to the functional role rather than escalating ownership.",
812
+ ],
813
+ });
814
+ }
815
+ // 37.
816
+ if (sfBroadPriv.length > 0) {
817
+ findings.push({
818
+ id: "SNOWFLAKE_BROAD_PRIVILEGE_GRANT",
819
+ title: "Broad grant of IMPORTED PRIVILEGES / MANAGE GRANTS / APPLY MASKING POLICY",
820
+ severity: "HIGH",
821
+ evidence: ev(sfBroadPriv),
822
+ requiredActions: [
823
+ "Do not grant MANAGE GRANTS broadly — it lets a role re-grant any privilege (privilege escalation).",
824
+ "Limit IMPORTED PRIVILEGES on shares/SNOWFLAKE db to specific audited roles.",
825
+ "Never grant APPLY MASKING POLICY / APPLY ROW ACCESS POLICY to PUBLIC or generic user roles.",
826
+ ],
827
+ });
828
+ }
829
+ // 38.
830
+ if (sfProcExecOwner.length > 0) {
831
+ findings.push({
832
+ id: "SNOWFLAKE_PROCEDURE_EXECUTE_AS_OWNER",
833
+ title: "Stored procedure EXECUTE AS OWNER — SQL injection enables privilege escalation",
834
+ severity: "HIGH",
835
+ evidence: ev(sfProcExecOwner),
836
+ requiredActions: [
837
+ "Prefer EXECUTE AS CALLER unless owner's rights are strictly required.",
838
+ "For EXECUTE AS OWNER procedures, parameterize all SQL and never concatenate caller input.",
839
+ "Own such procedures with a least-privilege role and restrict who can CALL them.",
840
+ ],
841
+ });
842
+ }
843
+ // 39.
844
+ if (sfSecondaryRolesAll.length > 0) {
845
+ findings.push({
846
+ id: "SNOWFLAKE_DEFAULT_SECONDARY_ROLES_ALL",
847
+ title: "User DEFAULT_SECONDARY_ROLES = ('ALL') — every granted role active at login",
848
+ severity: "MEDIUM",
849
+ evidence: ev(sfSecondaryRolesAll),
850
+ requiredActions: [
851
+ "Avoid DEFAULT_SECONDARY_ROLES = ('ALL') for privileged or service users — it activates all roles simultaneously.",
852
+ "Require explicit USE ROLE / USE SECONDARY ROLES so privileged actions are intentional.",
853
+ "Audit which users have ALL secondary roles and tighten the role hierarchy.",
854
+ ],
855
+ });
856
+ }
857
+ // 40.
858
+ if (sfPipeStageCred.length > 0) {
859
+ findings.push({
860
+ id: "SNOWFLAKE_PIPE_STAGE_INLINE_CRED",
861
+ title: "Pipe/stage configured with inline cloud credentials (AWS/Azure SAS token)",
862
+ severity: "CRITICAL",
863
+ evidence: ev(sfPipeStageCred),
864
+ requiredActions: [
865
+ "Remove AWS_KEY_ID/AWS_SECRET_KEY/AWS_TOKEN/AZURE_SAS_TOKEN from CREATE PIPE/STAGE and rotate them.",
866
+ "Use a STORAGE INTEGRATION (and NOTIFICATION INTEGRATION for pipes) instead of inline credentials.",
867
+ "Enforce REQUIRE_STORAGE_INTEGRATION_FOR_STAGE_CREATION = TRUE at the account level.",
868
+ ],
869
+ });
870
+ }
871
+ // 41.
872
+ if (sfShareWildcard.length > 0) {
873
+ findings.push({
874
+ id: "SNOWFLAKE_SHARE_WILDCARD_PUBLIC",
875
+ title: "Data share added to wildcard accounts or published as a public listing",
876
+ severity: "HIGH",
877
+ evidence: ev(sfShareWildcard),
878
+ requiredActions: [
879
+ "Never ALTER SHARE … ADD ACCOUNTS = '*' — share only with explicitly named, expected accounts.",
880
+ "Review any public Marketplace LISTING for unintended exposure of sensitive data.",
881
+ "Apply secure views / row-access policies to shared objects before sharing.",
882
+ ],
883
+ });
884
+ }
885
+ // 42. Password policy absent.
886
+ if (sfUsage.length > 0 && sfPwPolicyPresent.length === 0) {
887
+ findings.push({
888
+ id: "SNOWFLAKE_NO_PASSWORD_POLICY",
889
+ title: "Snowflake in use but no password policy (min length / lockout) defined",
890
+ severity: "MEDIUM",
891
+ requiredActions: [
892
+ "Create a PASSWORD POLICY with PASSWORD_MIN_LENGTH >= 14 and complexity requirements.",
893
+ "Set PASSWORD_MAX_RETRIES / lockout and PASSWORD_MAX_AGE_DAYS, then ALTER ACCOUNT SET PASSWORD POLICY.",
894
+ "Prefer SSO/key-pair auth and reserve passwords for break-glass accounts only.",
895
+ ],
896
+ });
897
+ }
898
+ // 43. Access/login history not used (LOW heuristic).
899
+ if (sfUsage.length > 0 && sfHistoryUsage.length === 0) {
900
+ findings.push({
901
+ id: "SNOWFLAKE_NO_ACCESS_HISTORY_MONITORING",
902
+ title: "No use of ACCESS_HISTORY / LOGIN_HISTORY — privileged access goes unmonitored",
903
+ severity: "LOW",
904
+ requiredActions: [
905
+ "Query SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY to track column-level data access on sensitive tables.",
906
+ "Monitor LOGIN_HISTORY for failed logins, new IPs, and client types; alert via a SIEM.",
907
+ "Retain and export these views beyond the default 365-day window for audit.",
908
+ ],
909
+ });
910
+ }
911
+ // 44. Warehouse AUTO_SUSPEND disabled (cost, LOW).
912
+ if (sfWhNoSuspend.length > 0) {
913
+ findings.push({
914
+ id: "SNOWFLAKE_WAREHOUSE_NO_AUTO_SUSPEND",
915
+ title: "Warehouse AUTO_SUSPEND = 0 — runaway compute / cost amplification",
916
+ severity: "LOW",
917
+ evidence: ev(sfWhNoSuspend),
918
+ requiredActions: [
919
+ "Set AUTO_SUSPEND to a small idle timeout (e.g. 60 seconds) on all warehouses.",
920
+ "Enable AUTO_RESUME and set resource monitors with credit quotas to cap spend.",
921
+ "Alert on warehouses that never suspend — they can mask abusive query activity.",
922
+ ],
923
+ });
924
+ }
925
+ // 45. Time Travel disabled on sensitive tables.
926
+ if (sfDataRetentionZero.length > 0) {
927
+ findings.push({
928
+ id: "SNOWFLAKE_DATA_RETENTION_ZERO",
929
+ title: "DATA_RETENTION_TIME_IN_DAYS = 0 — Time Travel disabled, no recovery from malicious deletes",
930
+ severity: "MEDIUM",
931
+ evidence: ev(sfDataRetentionZero),
932
+ requiredActions: [
933
+ "Set DATA_RETENTION_TIME_IN_DAYS >= 7 (Enterprise: up to 90) on sensitive tables/databases.",
934
+ "Time Travel enables recovery from accidental or malicious DROP/TRUNCATE/UPDATE.",
935
+ "Combine with fail-safe and external backups for regulated data.",
936
+ ],
937
+ });
938
+ }
939
+ // 46. Periodic data rekeying disabled.
940
+ if (sfRekeyingOff.length > 0) {
941
+ findings.push({
942
+ id: "SNOWFLAKE_PERIODIC_REKEYING_OFF",
943
+ title: "PERIODIC_DATA_REKEYING = FALSE — encryption keys not rotated annually",
944
+ severity: "LOW",
945
+ evidence: ev(sfRekeyingOff),
946
+ requiredActions: [
947
+ "Set PERIODIC_DATA_REKEYING = TRUE so Snowflake re-encrypts data with new keys yearly.",
948
+ "Document key rotation for SOC 2 / PCI DSS evidence.",
949
+ "Use Tri-Secret Secure with a customer-managed key for regulated workloads.",
950
+ ],
951
+ });
952
+ }
953
+ return findings;
954
+ }