agent-security-scanner-mcp 3.0.0 → 3.1.0

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 (233) hide show
  1. package/README.md +398 -754
  2. package/analyzer.py +51 -7
  3. package/index.js +173 -431
  4. package/package.json +3 -6
  5. package/regex_fallback.py +66 -0
  6. package/rules/__init__.py +124 -36
  7. package/rules/generic/secrets/gitleaks/adafruit-api-key.yaml +27 -0
  8. package/rules/generic/secrets/gitleaks/adobe-client-id.yaml +27 -0
  9. package/rules/generic/secrets/gitleaks/adobe-client-secret.yaml +27 -0
  10. package/rules/generic/secrets/gitleaks/age-secret-key.yaml +27 -0
  11. package/rules/generic/secrets/gitleaks/airtable-api-key.yaml +27 -0
  12. package/rules/generic/secrets/gitleaks/algolia-api-key.yaml +27 -0
  13. package/rules/generic/secrets/gitleaks/alibaba-access-key-id.yaml +27 -0
  14. package/rules/generic/secrets/gitleaks/alibaba-secret-key.yaml +27 -0
  15. package/rules/generic/secrets/gitleaks/asana-client-id.yaml +27 -0
  16. package/rules/generic/secrets/gitleaks/asana-client-secret.yaml +27 -0
  17. package/rules/generic/secrets/gitleaks/atlassian-api-token.yaml +27 -0
  18. package/rules/generic/secrets/gitleaks/authress-service-client-access-key.yaml +27 -0
  19. package/rules/generic/secrets/gitleaks/aws-access-token.yaml +27 -0
  20. package/rules/generic/secrets/gitleaks/beamer-api-token.yaml +27 -0
  21. package/rules/generic/secrets/gitleaks/bitbucket-client-id.yaml +27 -0
  22. package/rules/generic/secrets/gitleaks/bitbucket-client-secret.yaml +27 -0
  23. package/rules/generic/secrets/gitleaks/bittrex-access-key.yaml +27 -0
  24. package/rules/generic/secrets/gitleaks/bittrex-secret-key.yaml +27 -0
  25. package/rules/generic/secrets/gitleaks/clojars-api-token.yaml +27 -0
  26. package/rules/generic/secrets/gitleaks/cloudflare-api-key.yaml +27 -0
  27. package/rules/generic/secrets/gitleaks/cloudflare-global-api-key.yaml +27 -0
  28. package/rules/generic/secrets/gitleaks/cloudflare-origin-ca-key.yaml +27 -0
  29. package/rules/generic/secrets/gitleaks/codecov-access-token.yaml +27 -0
  30. package/rules/generic/secrets/gitleaks/coinbase-access-token.yaml +27 -0
  31. package/rules/generic/secrets/gitleaks/confluent-access-token.yaml +27 -0
  32. package/rules/generic/secrets/gitleaks/confluent-secret-key.yaml +27 -0
  33. package/rules/generic/secrets/gitleaks/contentful-delivery-api-token.yaml +27 -0
  34. package/rules/generic/secrets/gitleaks/databricks-api-token.yaml +27 -0
  35. package/rules/generic/secrets/gitleaks/datadog-access-token.yaml +27 -0
  36. package/rules/generic/secrets/gitleaks/defined-networking-api-token.yaml +27 -0
  37. package/rules/generic/secrets/gitleaks/digitalocean-access-token.yaml +27 -0
  38. package/rules/generic/secrets/gitleaks/digitalocean-pat.yaml +27 -0
  39. package/rules/generic/secrets/gitleaks/digitalocean-refresh-token.yaml +27 -0
  40. package/rules/generic/secrets/gitleaks/discord-api-token.yaml +27 -0
  41. package/rules/generic/secrets/gitleaks/discord-client-id.yaml +27 -0
  42. package/rules/generic/secrets/gitleaks/discord-client-secret.yaml +27 -0
  43. package/rules/generic/secrets/gitleaks/doppler-api-token.yaml +27 -0
  44. package/rules/generic/secrets/gitleaks/droneci-access-token.yaml +27 -0
  45. package/rules/generic/secrets/gitleaks/dropbox-api-token.yaml +27 -0
  46. package/rules/generic/secrets/gitleaks/dropbox-long-lived-api-token.yaml +27 -0
  47. package/rules/generic/secrets/gitleaks/dropbox-short-lived-api-token.yaml +27 -0
  48. package/rules/generic/secrets/gitleaks/duffel-api-token.yaml +27 -0
  49. package/rules/generic/secrets/gitleaks/dynatrace-api-token.yaml +27 -0
  50. package/rules/generic/secrets/gitleaks/easypost-api-token.yaml +27 -0
  51. package/rules/generic/secrets/gitleaks/easypost-test-api-token.yaml +27 -0
  52. package/rules/generic/secrets/gitleaks/etsy-access-token.yaml +27 -0
  53. package/rules/generic/secrets/gitleaks/facebook-access-token.yaml +27 -0
  54. package/rules/generic/secrets/gitleaks/facebook-page-access-token.yaml +27 -0
  55. package/rules/generic/secrets/gitleaks/facebook-secret.yaml +27 -0
  56. package/rules/generic/secrets/gitleaks/facebook.yaml +27 -0
  57. package/rules/generic/secrets/gitleaks/fastly-api-token.yaml +27 -0
  58. package/rules/generic/secrets/gitleaks/finicity-api-token.yaml +27 -0
  59. package/rules/generic/secrets/gitleaks/finicity-client-secret.yaml +27 -0
  60. package/rules/generic/secrets/gitleaks/finnhub-access-token.yaml +27 -0
  61. package/rules/generic/secrets/gitleaks/flickr-access-token.yaml +27 -0
  62. package/rules/generic/secrets/gitleaks/flutterwave-encryption-key.yaml +27 -0
  63. package/rules/generic/secrets/gitleaks/flutterwave-public-key.yaml +27 -0
  64. package/rules/generic/secrets/gitleaks/flutterwave-secret-key.yaml +27 -0
  65. package/rules/generic/secrets/gitleaks/frameio-api-token.yaml +27 -0
  66. package/rules/generic/secrets/gitleaks/freshbooks-access-token.yaml +27 -0
  67. package/rules/generic/secrets/gitleaks/gcp-api-key.yaml +27 -0
  68. package/rules/generic/secrets/gitleaks/generic-api-key.yaml +76 -0
  69. package/rules/generic/secrets/gitleaks/github-app-token.yaml +27 -0
  70. package/rules/generic/secrets/gitleaks/github-fine-grained-pat.yaml +27 -0
  71. package/rules/generic/secrets/gitleaks/github-oauth.yaml +27 -0
  72. package/rules/generic/secrets/gitleaks/github-pat.yaml +27 -0
  73. package/rules/generic/secrets/gitleaks/github-refresh-token.yaml +27 -0
  74. package/rules/generic/secrets/gitleaks/gitlab-pat.yaml +27 -0
  75. package/rules/generic/secrets/gitleaks/gitlab-ptt.yaml +27 -0
  76. package/rules/generic/secrets/gitleaks/gitlab-rrt.yaml +27 -0
  77. package/rules/generic/secrets/gitleaks/gitter-access-token.yaml +27 -0
  78. package/rules/generic/secrets/gitleaks/gocardless-api-token.yaml +27 -0
  79. package/rules/generic/secrets/gitleaks/grafana-api-key.yaml +27 -0
  80. package/rules/generic/secrets/gitleaks/grafana-cloud-api-token.yaml +27 -0
  81. package/rules/generic/secrets/gitleaks/grafana-service-account-token.yaml +27 -0
  82. package/rules/generic/secrets/gitleaks/harness-api-key.yaml +27 -0
  83. package/rules/generic/secrets/gitleaks/hashicorp-tf-api-token.yaml +27 -0
  84. package/rules/generic/secrets/gitleaks/hashicorp-tf-password.yaml +31 -0
  85. package/rules/generic/secrets/gitleaks/heroku-api-key.yaml +27 -0
  86. package/rules/generic/secrets/gitleaks/hubspot-api-key.yaml +27 -0
  87. package/rules/generic/secrets/gitleaks/huggingface-access-token.yaml +27 -0
  88. package/rules/generic/secrets/gitleaks/huggingface-organization-api-token.yaml +27 -0
  89. package/rules/generic/secrets/gitleaks/infracost-api-token.yaml +27 -0
  90. package/rules/generic/secrets/gitleaks/intercom-api-key.yaml +27 -0
  91. package/rules/generic/secrets/gitleaks/intra42-client-secret.yaml +27 -0
  92. package/rules/generic/secrets/gitleaks/jfrog-api-key.yaml +27 -0
  93. package/rules/generic/secrets/gitleaks/jfrog-identity-token.yaml +27 -0
  94. package/rules/generic/secrets/gitleaks/jwt-base64.yaml +27 -0
  95. package/rules/generic/secrets/gitleaks/jwt.yaml +27 -0
  96. package/rules/generic/secrets/gitleaks/kraken-access-token.yaml +27 -0
  97. package/rules/generic/secrets/gitleaks/kucoin-access-token.yaml +27 -0
  98. package/rules/generic/secrets/gitleaks/kucoin-secret-key.yaml +27 -0
  99. package/rules/generic/secrets/gitleaks/launchdarkly-access-token.yaml +27 -0
  100. package/rules/generic/secrets/gitleaks/linear-api-key.yaml +27 -0
  101. package/rules/generic/secrets/gitleaks/linear-client-secret.yaml +27 -0
  102. package/rules/generic/secrets/gitleaks/linkedin-client-id.yaml +27 -0
  103. package/rules/generic/secrets/gitleaks/linkedin-client-secret.yaml +27 -0
  104. package/rules/generic/secrets/gitleaks/lob-api-key.yaml +27 -0
  105. package/rules/generic/secrets/gitleaks/lob-pub-api-key.yaml +27 -0
  106. package/rules/generic/secrets/gitleaks/mailchimp-api-key.yaml +27 -0
  107. package/rules/generic/secrets/gitleaks/mailgun-private-api-token.yaml +27 -0
  108. package/rules/generic/secrets/gitleaks/mailgun-pub-key.yaml +27 -0
  109. package/rules/generic/secrets/gitleaks/mailgun-signing-key.yaml +27 -0
  110. package/rules/generic/secrets/gitleaks/mapbox-api-token.yaml +27 -0
  111. package/rules/generic/secrets/gitleaks/mattermost-access-token.yaml +27 -0
  112. package/rules/generic/secrets/gitleaks/messagebird-api-token.yaml +27 -0
  113. package/rules/generic/secrets/gitleaks/messagebird-client-id.yaml +27 -0
  114. package/rules/generic/secrets/gitleaks/microsoft-teams-webhook.yaml +27 -0
  115. package/rules/generic/secrets/gitleaks/netlify-access-token.yaml +27 -0
  116. package/rules/generic/secrets/gitleaks/new-relic-browser-api-token.yaml +27 -0
  117. package/rules/generic/secrets/gitleaks/new-relic-insert-key.yaml +27 -0
  118. package/rules/generic/secrets/gitleaks/new-relic-user-api-id.yaml +27 -0
  119. package/rules/generic/secrets/gitleaks/new-relic-user-api-key.yaml +27 -0
  120. package/rules/generic/secrets/gitleaks/npm-access-token.yaml +27 -0
  121. package/rules/generic/secrets/gitleaks/nytimes-access-token.yaml +27 -0
  122. package/rules/generic/secrets/gitleaks/okta-access-token.yaml +27 -0
  123. package/rules/generic/secrets/gitleaks/openai-api-key.yaml +27 -0
  124. package/rules/generic/secrets/gitleaks/plaid-api-token.yaml +27 -0
  125. package/rules/generic/secrets/gitleaks/plaid-client-id.yaml +27 -0
  126. package/rules/generic/secrets/gitleaks/plaid-secret-key.yaml +27 -0
  127. package/rules/generic/secrets/gitleaks/planetscale-api-token.yaml +27 -0
  128. package/rules/generic/secrets/gitleaks/planetscale-oauth-token.yaml +27 -0
  129. package/rules/generic/secrets/gitleaks/planetscale-password.yaml +27 -0
  130. package/rules/generic/secrets/gitleaks/postman-api-token.yaml +27 -0
  131. package/rules/generic/secrets/gitleaks/prefect-api-token.yaml +27 -0
  132. package/rules/generic/secrets/gitleaks/private-key.yaml +27 -0
  133. package/rules/generic/secrets/gitleaks/pulumi-api-token.yaml +27 -0
  134. package/rules/generic/secrets/gitleaks/pypi-upload-token.yaml +27 -0
  135. package/rules/generic/secrets/gitleaks/rapidapi-access-token.yaml +27 -0
  136. package/rules/generic/secrets/gitleaks/readme-api-token.yaml +27 -0
  137. package/rules/generic/secrets/gitleaks/rubygems-api-token.yaml +27 -0
  138. package/rules/generic/secrets/gitleaks/scalingo-api-token.yaml +27 -0
  139. package/rules/generic/secrets/gitleaks/sendbird-access-id.yaml +27 -0
  140. package/rules/generic/secrets/gitleaks/sendbird-access-token.yaml +27 -0
  141. package/rules/generic/secrets/gitleaks/sendgrid-api-token.yaml +27 -0
  142. package/rules/generic/secrets/gitleaks/sendinblue-api-token.yaml +27 -0
  143. package/rules/generic/secrets/gitleaks/sentry-access-token.yaml +27 -0
  144. package/rules/generic/secrets/gitleaks/shippo-api-token.yaml +27 -0
  145. package/rules/generic/secrets/gitleaks/shopify-access-token.yaml +27 -0
  146. package/rules/generic/secrets/gitleaks/shopify-custom-access-token.yaml +27 -0
  147. package/rules/generic/secrets/gitleaks/shopify-private-app-access-token.yaml +27 -0
  148. package/rules/generic/secrets/gitleaks/shopify-shared-secret.yaml +27 -0
  149. package/rules/generic/secrets/gitleaks/sidekiq-secret.yaml +27 -0
  150. package/rules/generic/secrets/gitleaks/sidekiq-sensitive-url.yaml +27 -0
  151. package/rules/generic/secrets/gitleaks/slack-app-token.yaml +27 -0
  152. package/rules/generic/secrets/gitleaks/slack-bot-token.yaml +27 -0
  153. package/rules/generic/secrets/gitleaks/slack-config-access-token.yaml +27 -0
  154. package/rules/generic/secrets/gitleaks/slack-config-refresh-token.yaml +27 -0
  155. package/rules/generic/secrets/gitleaks/slack-legacy-bot-token.yaml +27 -0
  156. package/rules/generic/secrets/gitleaks/slack-legacy-token.yaml +27 -0
  157. package/rules/generic/secrets/gitleaks/slack-legacy-workspace-token.yaml +27 -0
  158. package/rules/generic/secrets/gitleaks/slack-user-token.yaml +27 -0
  159. package/rules/generic/secrets/gitleaks/slack-webhook-url.yaml +27 -0
  160. package/rules/generic/secrets/gitleaks/snyk-api-token.yaml +27 -0
  161. package/rules/generic/secrets/gitleaks/square-access-token.yaml +27 -0
  162. package/rules/generic/secrets/gitleaks/squarespace-access-token.yaml +27 -0
  163. package/rules/generic/secrets/gitleaks/stripe-access-token.yaml +27 -0
  164. package/rules/generic/secrets/gitleaks/sumologic-access-id.yaml +27 -0
  165. package/rules/generic/secrets/gitleaks/sumologic-access-token.yaml +27 -0
  166. package/rules/generic/secrets/gitleaks/telegram-bot-api-token.yaml +27 -0
  167. package/rules/generic/secrets/gitleaks/travisci-access-token.yaml +27 -0
  168. package/rules/generic/secrets/gitleaks/twilio-api-key.yaml +27 -0
  169. package/rules/generic/secrets/gitleaks/twitch-api-token.yaml +27 -0
  170. package/rules/generic/secrets/gitleaks/twitter-access-secret.yaml +27 -0
  171. package/rules/generic/secrets/gitleaks/twitter-access-token.yaml +27 -0
  172. package/rules/generic/secrets/gitleaks/twitter-api-key.yaml +27 -0
  173. package/rules/generic/secrets/gitleaks/twitter-api-secret.yaml +27 -0
  174. package/rules/generic/secrets/gitleaks/twitter-bearer-token.yaml +27 -0
  175. package/rules/generic/secrets/gitleaks/typeform-api-token.yaml +27 -0
  176. package/rules/generic/secrets/gitleaks/vault-batch-token.yaml +27 -0
  177. package/rules/generic/secrets/gitleaks/vault-service-token.yaml +27 -0
  178. package/rules/generic/secrets/gitleaks/yandex-access-token.yaml +27 -0
  179. package/rules/generic/secrets/gitleaks/yandex-api-key.yaml +27 -0
  180. package/rules/generic/secrets/gitleaks/yandex-aws-access-token.yaml +27 -0
  181. package/rules/generic/secrets/gitleaks/zendesk-secret-key.yaml +27 -0
  182. package/rules/generic/secrets/security/detected-amazon-mws-auth-token.yaml +26 -0
  183. package/rules/generic/secrets/security/detected-artifactory-password.yaml +47 -0
  184. package/rules/generic/secrets/security/detected-artifactory-token.yaml +44 -0
  185. package/rules/generic/secrets/security/detected-aws-access-key-id-value.yaml +29 -0
  186. package/rules/generic/secrets/security/detected-aws-account-id.yaml +58 -0
  187. package/rules/generic/secrets/security/detected-aws-appsync-graphql-key.yaml +27 -0
  188. package/rules/generic/secrets/security/detected-aws-secret-access-key.yaml +30 -0
  189. package/rules/generic/secrets/security/detected-aws-session-token.yaml +31 -0
  190. package/rules/generic/secrets/security/detected-bcrypt-hash.yaml +25 -0
  191. package/rules/generic/secrets/security/detected-codeclimate.yaml +27 -0
  192. package/rules/generic/secrets/security/detected-etc-shadow.yaml +27 -0
  193. package/rules/generic/secrets/security/detected-facebook-access-token.yaml +29 -0
  194. package/rules/generic/secrets/security/detected-facebook-oauth.yaml +27 -0
  195. package/rules/generic/secrets/security/detected-generic-api-key.yaml +29 -0
  196. package/rules/generic/secrets/security/detected-generic-secret.yaml +30 -0
  197. package/rules/generic/secrets/security/detected-github-token.yaml +47 -0
  198. package/rules/generic/secrets/security/detected-google-api-key.yaml +29 -0
  199. package/rules/generic/secrets/security/detected-google-cloud-api-key.yaml +27 -0
  200. package/rules/generic/secrets/security/detected-google-gcm-service-account.yaml +27 -0
  201. package/rules/generic/secrets/security/detected-google-oauth-access-token.yaml +26 -0
  202. package/rules/generic/secrets/security/detected-google-oauth.yaml +26 -0
  203. package/rules/generic/secrets/security/detected-heroku-api-key.yaml +27 -0
  204. package/rules/generic/secrets/security/detected-hockeyapp.yaml +27 -0
  205. package/rules/generic/secrets/security/detected-jwt-token.yaml +25 -0
  206. package/rules/generic/secrets/security/detected-kolide-api-key.yaml +25 -0
  207. package/rules/generic/secrets/security/detected-mailchimp-api-key.yaml +26 -0
  208. package/rules/generic/secrets/security/detected-mailgun-api-key.yaml +26 -0
  209. package/rules/generic/secrets/security/detected-npm-registry-auth-token.yaml +33 -0
  210. package/rules/generic/secrets/security/detected-onfido-live-api-token.yaml +20 -0
  211. package/rules/generic/secrets/security/detected-outlook-team.yaml +27 -0
  212. package/rules/generic/secrets/security/detected-paypal-braintree-access-token.yaml +27 -0
  213. package/rules/generic/secrets/security/detected-pgp-private-key-block.yaml +28 -0
  214. package/rules/generic/secrets/security/detected-picatic-api-key.yaml +26 -0
  215. package/rules/generic/secrets/security/detected-private-key.yaml +39 -0
  216. package/rules/generic/secrets/security/detected-sauce-token.yaml +27 -0
  217. package/rules/generic/secrets/security/detected-sendgrid-api-key.yaml +27 -0
  218. package/rules/generic/secrets/security/detected-slack-token.yaml +28 -0
  219. package/rules/generic/secrets/security/detected-slack-webhook.yaml +27 -0
  220. package/rules/generic/secrets/security/detected-snyk-api-key.yaml +26 -0
  221. package/rules/generic/secrets/security/detected-softlayer-api-key.yaml +27 -0
  222. package/rules/generic/secrets/security/detected-sonarqube-docs-api-key.yaml +40 -0
  223. package/rules/generic/secrets/security/detected-square-access-token.yaml +26 -0
  224. package/rules/generic/secrets/security/detected-square-oauth-secret.yaml +27 -0
  225. package/rules/generic/secrets/security/detected-ssh-password.yaml +27 -0
  226. package/rules/generic/secrets/security/detected-stripe-api-key.yaml +26 -0
  227. package/rules/generic/secrets/security/detected-stripe-restricted-api-key.yaml +26 -0
  228. package/rules/generic/secrets/security/detected-telegram-bot-api-key.yaml +30 -0
  229. package/rules/generic/secrets/security/detected-twilio-api-key.yaml +26 -0
  230. package/rules/generic/secrets/security/detected-username-and-password-in-uri.yaml +35 -0
  231. package/rules/generic/secrets/security/google-maps-apikeyleak.yaml +25 -0
  232. package/rules/prompt-injection.security.yaml +4 -0
  233. package/rules/python/flask/security/injection/flask-injection-sinks.yaml +352 -0
package/index.js CHANGED
@@ -21,6 +21,21 @@ try {
21
21
  __dirname = process.cwd();
22
22
  }
23
23
 
24
+ // Helper: return correct env var access syntax per language
25
+ function envVarReplacement(envName, lang) {
26
+ switch(lang) {
27
+ case 'python': return `os.environ.get("${envName}")`;
28
+ case 'go': return `os.Getenv("${envName}")`;
29
+ case 'java': return `System.getenv("${envName}")`;
30
+ case 'php': return `getenv('${envName}')`;
31
+ case 'ruby': return `ENV["${envName}"]`;
32
+ case 'csharp': return `Environment.GetEnvironmentVariable("${envName}")`;
33
+ case 'rust': return `std::env::var("${envName}").unwrap_or_default()`;
34
+ case 'c': case 'cpp': return `getenv("${envName}")`;
35
+ default: return `process.env.${envName}`;
36
+ }
37
+ }
38
+
24
39
  // Security fix templates - comprehensive coverage for 165+ rules
25
40
  const FIX_TEMPLATES = {
26
41
  // ===========================================
@@ -28,73 +43,7 @@ const FIX_TEMPLATES = {
28
43
  // ===========================================
29
44
  "sql-injection": {
30
45
  description: "Use parameterized queries instead of string concatenation",
31
- patterns: [
32
- // Python f-strings: cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
33
- {
34
- match: /f["'].*(?:SELECT|INSERT|UPDATE|DELETE).*\{(\w+)\}.*["']/i,
35
- fix: (line) => {
36
- // Extract the query and variable
37
- const match = line.match(/(\w+\.(?:execute|query|run))\s*\(\s*f(["'])(.*?)(?:SELECT|INSERT|UPDATE|DELETE)(.*?)\{(\w+)\}(.*?)\2/i);
38
- if (match) {
39
- const [, method, quote, prefix, queryStart, varName, suffix] = match;
40
- // Reconstruct as parameterized query
41
- const cleanPrefix = prefix.replace(/\{[^}]+\}/g, '?');
42
- const cleanSuffix = suffix.replace(/\{[^}]+\}/g, '?');
43
- return line.replace(
44
- /f(["']).*\1/,
45
- `"${cleanPrefix}${queryStart.trim().toUpperCase()}${cleanSuffix}?", (${varName},)`
46
- );
47
- }
48
- // Simpler fallback for f-strings
49
- return line.replace(/f(["'])(.*?)\{(\w+)\}(.*?)\1/, '"$2?$4", ($3,)');
50
- },
51
- languages: ['python']
52
- },
53
- // Python .format(): "SELECT ... WHERE id = {}".format(user_id)
54
- {
55
- match: /["'].*(?:SELECT|INSERT|UPDATE|DELETE).*\{\}.*["']\.format\s*\(/i,
56
- fix: (line) => {
57
- return line.replace(
58
- /(["'])(.*?)\{\}(.*?)\1\.format\s*\(\s*(\w+)\s*\)/,
59
- '"$2?$3", [$4]'
60
- );
61
- },
62
- languages: ['python']
63
- },
64
- // Python % formatting: "SELECT ... WHERE id = %s" % user_id
65
- {
66
- match: /["'].*(?:SELECT|INSERT|UPDATE|DELETE).*%s.*["']\s*%\s*\(/i,
67
- fix: (line) => {
68
- return line.replace(
69
- /(["'])(.*?)%s(.*?)\1\s*%\s*\(\s*(\w+)\s*,?\s*\)/,
70
- '"$2?$3", [$4]'
71
- );
72
- },
73
- languages: ['python']
74
- },
75
- // JS template literals: `SELECT * FROM users WHERE id = ${userId}`
76
- {
77
- match: /`.*(?:SELECT|INSERT|UPDATE|DELETE).*\$\{.*\}.*`/i,
78
- fix: (line) => {
79
- return line.replace(
80
- /`(.*?)\$\{(\w+)\}(.*?)`/,
81
- '"$1?$3", [$2]'
82
- );
83
- },
84
- languages: ['javascript', 'typescript']
85
- },
86
- // Simple concatenation (no quotes inside): "SELECT ... WHERE id = " + userId
87
- {
88
- match: /["'](?:SELECT|INSERT|UPDATE|DELETE)[^"']+["']\s*\+\s*\w+(?!\s*\+\s*["'])/i,
89
- fix: (line) => {
90
- return line.replace(
91
- /(["'])((?:SELECT|INSERT|UPDATE|DELETE)[^"']+)\1\s*\+\s*(\w+)/i,
92
- '"$2?", [$3]'
93
- );
94
- },
95
- languages: ['javascript', 'python', 'java', 'go', 'ruby', 'php']
96
- }
97
- ]
46
+ fix: (line) => line.replace(/["']([^"']*)\s*["']\s*\+\s*(\w+)/, '"$1?", [$2]')
98
47
  },
99
48
  "nosql-injection": {
100
49
  description: "Sanitize MongoDB query inputs",
@@ -170,92 +119,60 @@ const FIX_TEMPLATES = {
170
119
  // ===========================================
171
120
  "hardcoded": {
172
121
  description: "Use environment variables",
173
- fix: (line, lang) => {
174
- if (lang === 'python') {
175
- return line.replace(/=\s*["'][^"']+["']/, '= os.environ.get("SECRET")');
176
- }
177
- return line.replace(/=\s*["'][^"']+["']/, '= process.env.SECRET');
178
- }
122
+ fix: (line, lang) => line.replace(/=\s*["'][^"']+["']/, `= ${envVarReplacement("SECRET", lang)}`)
179
123
  },
180
124
  "api-key": {
181
125
  description: "Use environment variables for API keys",
182
- fix: (line, lang) => {
183
- if (lang === 'python') return line.replace(/=\s*["'][^"']+["']/, '= os.environ.get("API_KEY")');
184
- if (lang === 'go') return line.replace(/=\s*["'][^"']+["']/, '= os.Getenv("API_KEY")');
185
- return line.replace(/=\s*["'][^"']+["']/, '= process.env.API_KEY');
186
- }
126
+ fix: (line, lang) => line.replace(/=\s*["'][^"']+["']/, `= ${envVarReplacement("API_KEY", lang)}`)
187
127
  },
188
128
  "password": {
189
129
  description: "Use environment variables for passwords",
190
- fix: (line, lang) => {
191
- if (lang === 'python') return line.replace(/=\s*["'][^"']+["']/, '= os.environ.get("PASSWORD")');
192
- if (lang === 'go') return line.replace(/=\s*["'][^"']+["']/, '= os.Getenv("PASSWORD")');
193
- return line.replace(/=\s*["'][^"']+["']/, '= process.env.PASSWORD');
194
- }
130
+ fix: (line, lang) => line.replace(/=\s*["'][^"']+["']/, `= ${envVarReplacement("PASSWORD", lang)}`)
195
131
  },
196
132
  "secret-key": {
197
133
  description: "Use environment variables for secret keys",
198
- fix: (line, lang) => {
199
- if (lang === 'python') return line.replace(/=\s*["'][^"']+["']/, '= os.environ.get("SECRET_KEY")');
200
- return line.replace(/=\s*["'][^"']+["']/, '= process.env.SECRET_KEY');
201
- }
134
+ fix: (line, lang) => line.replace(/=\s*["'][^"']+["']/, `= ${envVarReplacement("SECRET_KEY", lang)}`)
202
135
  },
203
136
  "aws-access": {
204
137
  description: "Use AWS credentials from environment or IAM roles",
205
- fix: (line) => line.replace(/=\s*["']AKIA[^"']+["']/, '= os.environ.get("AWS_ACCESS_KEY_ID")')
138
+ fix: (line, lang) => line.replace(/=\s*["']AKIA[^"']+["']/, `= ${envVarReplacement("AWS_ACCESS_KEY_ID", lang)}`)
206
139
  },
207
140
  "aws-secret": {
208
141
  description: "Use AWS credentials from environment or IAM roles",
209
- fix: (line) => line.replace(/=\s*["'][^"']{40}["']/, '= os.environ.get("AWS_SECRET_ACCESS_KEY")')
142
+ fix: (line, lang) => line.replace(/=\s*["'][^"']{40}["']/, `= ${envVarReplacement("AWS_SECRET_ACCESS_KEY", lang)}`)
210
143
  },
211
144
  "stripe": {
212
145
  description: "Use environment variables for Stripe keys",
213
- fix: (line, lang) => {
214
- if (lang === 'python') return line.replace(/=\s*["']sk_(live|test)_[^"']+["']/, '= os.environ.get("STRIPE_SECRET_KEY")');
215
- return line.replace(/=\s*["']sk_(live|test)_[^"']+["']/, '= process.env.STRIPE_SECRET_KEY');
216
- }
146
+ fix: (line, lang) => line.replace(/=\s*["']sk_(live|test)_[^"']+["']/, `= ${envVarReplacement("STRIPE_SECRET_KEY", lang)}`)
217
147
  },
218
148
  "github": {
219
149
  description: "Use environment variables for GitHub tokens",
220
- fix: (line, lang) => {
221
- if (lang === 'python') return line.replace(/=\s*["'](ghp_|github_pat_)[^"']+["']/, '= os.environ.get("GITHUB_TOKEN")');
222
- return line.replace(/=\s*["'](ghp_|github_pat_)[^"']+["']/, '= process.env.GITHUB_TOKEN');
223
- }
150
+ fix: (line, lang) => line.replace(/=\s*["'](ghp_|github_pat_)[^"']+["']/, `= ${envVarReplacement("GITHUB_TOKEN", lang)}`)
224
151
  },
225
152
  "openai": {
226
153
  description: "Use environment variables for OpenAI keys",
227
- fix: (line, lang) => {
228
- if (lang === 'python') return line.replace(/=\s*["']sk-[^"']+["']/, '= os.environ.get("OPENAI_API_KEY")');
229
- return line.replace(/=\s*["']sk-[^"']+["']/, '= process.env.OPENAI_API_KEY');
230
- }
154
+ fix: (line, lang) => line.replace(/=\s*["']sk-[^"']+["']/, `= ${envVarReplacement("OPENAI_API_KEY", lang)}`)
231
155
  },
232
156
  "slack": {
233
157
  description: "Use environment variables for Slack tokens",
234
- fix: (line, lang) => {
235
- if (lang === 'python') return line.replace(/=\s*["']xox[baprs]-[^"']+["']/, '= os.environ.get("SLACK_TOKEN")');
236
- return line.replace(/=\s*["']xox[baprs]-[^"']+["']/, '= process.env.SLACK_TOKEN');
237
- }
158
+ fix: (line, lang) => line.replace(/=\s*["']xox[baprs]-[^"']+["']/, `= ${envVarReplacement("SLACK_TOKEN", lang)}`)
238
159
  },
239
160
  "jwt-token": {
240
161
  description: "Use environment variables for JWT secrets",
241
- fix: (line, lang) => {
242
- if (lang === 'python') return line.replace(/=\s*["'][^"']+["']/, '= os.environ.get("JWT_SECRET")');
243
- return line.replace(/=\s*["'][^"']+["']/, '= process.env.JWT_SECRET');
244
- }
162
+ fix: (line, lang) => line.replace(/=\s*["'][^"']+["']/, `= ${envVarReplacement("JWT_SECRET", lang)}`)
245
163
  },
246
164
  "private-key": {
247
165
  description: "Load private keys from secure file or vault",
248
166
  fix: (line, lang) => {
249
- if (lang === 'python') return line.replace(/=\s*["']-----BEGIN[^"']+["']/, '= load_key_from_file(os.environ.get("PRIVATE_KEY_PATH"))');
167
+ if (lang === 'python') return line.replace(/=\s*["']-----BEGIN[^"']+["']/, `= load_key_from_file(${envVarReplacement("PRIVATE_KEY_PATH", lang)})`);
168
+ if (lang === 'go' || lang === 'java' || lang === 'csharp' || lang === 'rust' || lang === 'c' || lang === 'cpp')
169
+ return line.replace(/=\s*["']-----BEGIN[^"']+["']/, `= ${envVarReplacement("PRIVATE_KEY_PATH", lang)}`);
250
170
  return line.replace(/=\s*["']-----BEGIN[^"']+["']/, '= fs.readFileSync(process.env.PRIVATE_KEY_PATH)');
251
171
  }
252
172
  },
253
173
  "database-url": {
254
174
  description: "Use environment variables for database URLs",
255
- fix: (line, lang) => {
256
- if (lang === 'python') return line.replace(/=\s*["'][^"']+["']/, '= os.environ.get("DATABASE_URL")');
257
- return line.replace(/=\s*["'][^"']+["']/, '= process.env.DATABASE_URL');
258
- }
175
+ fix: (line, lang) => line.replace(/=\s*["'][^"']+["']/, `= ${envVarReplacement("DATABASE_URL", lang)}`)
259
176
  },
260
177
 
261
178
  // ===========================================
@@ -805,11 +722,20 @@ const FIX_TEMPLATES = {
805
722
 
806
723
  // Detect language from file extension
807
724
  function detectLanguage(filePath) {
725
+ // Check basename first for extensionless files like Dockerfile
726
+ const basename = filePath.split('/').pop().split('\\').pop().toLowerCase();
727
+ if (basename === 'dockerfile' || basename.startsWith('dockerfile.')) return 'dockerfile';
728
+
808
729
  const ext = filePath.split('.').pop().toLowerCase();
809
730
  const langMap = {
810
731
  'py': 'python', 'js': 'javascript', 'ts': 'typescript',
811
732
  'tsx': 'typescript', 'jsx': 'javascript', 'java': 'java',
812
733
  'go': 'go', 'rb': 'ruby', 'php': 'php',
734
+ 'cs': 'csharp', 'rs': 'rust', 'c': 'c', 'cpp': 'cpp',
735
+ 'cc': 'cpp', 'cxx': 'cpp', 'h': 'c', 'hpp': 'cpp',
736
+ 'tf': 'terraform', 'hcl': 'terraform',
737
+ 'yaml': 'generic', 'yml': 'generic',
738
+ 'sql': 'sql',
813
739
  // Prompt/text file extensions for prompt injection scanning
814
740
  'txt': 'generic', 'md': 'generic', 'prompt': 'generic',
815
741
  'jinja': 'generic', 'jinja2': 'generic', 'j2': 'generic'
@@ -831,96 +757,17 @@ function runAnalyzer(filePath) {
831
757
  }
832
758
  }
833
759
 
834
- // Validate that a fix produces valid syntax
835
- function validateFix(original, fixed, language) {
836
- // Rule 1: Fix must be different from original
837
- if (fixed === original || !fixed) {
838
- return { valid: false, reason: 'no_change' };
839
- }
840
-
841
- // Rule 2: Balanced quotes (ignore escaped quotes)
842
- const unescaped = fixed.replace(/\\["'`]/g, '');
843
- const singleQuotes = (unescaped.match(/'/g) || []).length;
844
- const doubleQuotes = (unescaped.match(/"/g) || []).length;
845
- const backticks = (unescaped.match(/`/g) || []).length;
846
- if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0 || backticks % 2 !== 0) {
847
- return { valid: false, reason: 'unbalanced_quotes' };
848
- }
849
-
850
- // Rule 3: Balanced brackets
851
- const brackets = { '(': 0, '[': 0, '{': 0 };
852
- const closers = { ')': '(', ']': '[', '}': '{' };
853
- for (const char of unescaped) {
854
- if (brackets[char] !== undefined) brackets[char]++;
855
- if (closers[char]) brackets[closers[char]]--;
856
- }
857
- if (Object.values(brackets).some(v => v !== 0)) {
858
- return { valid: false, reason: 'unbalanced_brackets' };
859
- }
860
-
861
- // Rule 4: No obvious syntax errors
862
- const badPatterns = [
863
- /""[^,\s\]);}]/, // empty string followed by unexpected char
864
- /\+\s*[)\]}]/, // + followed by closing bracket
865
- /,\s*\+/, // comma followed by +
866
- /\(\s*\+/, // open paren followed by +
867
- ];
868
- for (const pattern of badPatterns) {
869
- if (pattern.test(fixed)) {
870
- return { valid: false, reason: 'syntax_error' };
871
- }
872
- }
873
-
874
- return { valid: true };
875
- }
876
-
877
760
  // Generate fix suggestion for an issue
878
761
  function generateFix(issue, line, language) {
879
762
  const ruleId = issue.ruleId.toLowerCase();
880
763
 
881
- for (const [templateId, template] of Object.entries(FIX_TEMPLATES)) {
882
- if (!ruleId.includes(templateId)) continue;
883
-
884
- // New: handle patterns array
885
- if (template.patterns && Array.isArray(template.patterns)) {
886
- for (const pattern of template.patterns) {
887
- // Skip if language doesn't match
888
- if (pattern.languages && !pattern.languages.includes(language)) {
889
- continue;
890
- }
891
-
892
- // Skip if pattern doesn't match the line
893
- if (!pattern.match.test(line)) {
894
- continue;
895
- }
896
-
897
- // Try the fix
898
- const candidate = pattern.fix(line, language);
899
- const validation = validateFix(line, candidate, language);
900
-
901
- if (validation.valid) {
902
- return {
903
- description: template.description,
904
- original: line,
905
- fixed: candidate
906
- };
907
- }
908
- // If invalid, try next pattern
909
- }
910
- }
911
-
912
- // Fallback: old-style single fix function (backward compatible)
913
- if (template.fix && typeof template.fix === 'function') {
914
- const candidate = template.fix(line, language);
915
- const validation = validateFix(line, candidate, language);
916
-
917
- if (validation.valid) {
918
- return {
919
- description: template.description,
920
- original: line,
921
- fixed: candidate
922
- };
923
- }
764
+ for (const [pattern, template] of Object.entries(FIX_TEMPLATES)) {
765
+ if (ruleId.includes(pattern)) {
766
+ return {
767
+ description: template.description,
768
+ original: line,
769
+ fixed: template.fix(line, language)
770
+ };
924
771
  }
925
772
  }
926
773
 
@@ -949,88 +796,70 @@ export function createSandboxServer() {
949
796
  return server;
950
797
  }
951
798
 
952
- // SARIF (Static Analysis Results Interchange Format) conversion
953
- // Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
954
- function convertToSarif(filePath, language, issues) {
799
+ // Convert issues to SARIF 2.1.0 format
800
+ function toSarif(file_path, language, issues) {
955
801
  const severityToLevel = {
802
+ 'error': 'error',
956
803
  'ERROR': 'error',
804
+ 'warning': 'warning',
957
805
  'WARNING': 'warning',
958
- 'INFO': 'note',
959
- 'HINT': 'note'
806
+ 'info': 'note',
807
+ 'INFO': 'note'
960
808
  };
961
809
 
962
- // Build rules from unique rule IDs
810
+ // Build unique rules from issues
963
811
  const rulesMap = new Map();
964
- issues.forEach(issue => {
812
+ for (const issue of issues) {
965
813
  if (!rulesMap.has(issue.ruleId)) {
966
814
  rulesMap.set(issue.ruleId, {
967
815
  id: issue.ruleId,
968
- name: issue.ruleId.split('.').pop().replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
969
- shortDescription: {
970
- text: issue.message.replace(/^\[.*?\]\s*/, '') // Remove [RuleName] prefix
971
- },
816
+ shortDescription: { text: issue.message },
972
817
  defaultConfiguration: {
973
818
  level: severityToLevel[issue.severity] || 'warning'
974
819
  },
975
- properties: {
976
- tags: ['security'],
977
- ...(issue.metadata?.cwe && { 'security-severity': '7.0' }),
978
- },
979
- helpUri: issue.metadata?.references?.[0] || `https://cwe.mitre.org/data/definitions/${issue.metadata?.cwe?.replace('CWE-', '')}.html`
820
+ properties: issue.metadata || {}
980
821
  });
981
822
  }
982
- });
823
+ }
983
824
 
984
825
  // Build results
985
- const results = issues.map(issue => ({
986
- ruleId: issue.ruleId,
987
- level: severityToLevel[issue.severity] || 'warning',
988
- message: {
989
- text: issue.message
990
- },
991
- locations: [{
992
- physicalLocation: {
993
- artifactLocation: {
994
- uri: filePath,
995
- uriBaseId: '%SRCROOT%'
996
- },
997
- region: {
998
- startLine: (issue.line || 0) + 1, // SARIF uses 1-indexed lines
999
- startColumn: (issue.column || 0) + 1,
1000
- endLine: (issue.endLine || issue.line || 0) + 1,
1001
- endColumn: (issue.endColumn || issue.column || 0) + 1,
1002
- snippet: issue.line_content ? { text: issue.line_content } : undefined
826
+ const results = issues.map(issue => {
827
+ const result = {
828
+ ruleId: issue.ruleId,
829
+ level: severityToLevel[issue.severity] || 'warning',
830
+ message: { text: issue.message },
831
+ locations: [{
832
+ physicalLocation: {
833
+ artifactLocation: { uri: file_path },
834
+ region: {
835
+ startLine: (issue.line || 0) + 1,
836
+ startColumn: (issue.column || 0) + 1
837
+ }
1003
838
  }
1004
- }
1005
- }],
1006
- ...(issue.suggested_fix?.fixed && {
1007
- fixes: [{
1008
- description: {
1009
- text: issue.suggested_fix.description
1010
- },
839
+ }]
840
+ };
841
+
842
+ // Add fix if available
843
+ if (issue.suggested_fix && issue.suggested_fix.fixed) {
844
+ result.fixes = [{
845
+ description: { text: issue.suggested_fix.description || 'Apply security fix' },
1011
846
  artifactChanges: [{
1012
- artifactLocation: {
1013
- uri: filePath
1014
- },
847
+ artifactLocation: { uri: file_path },
1015
848
  replacements: [{
1016
849
  deletedRegion: {
1017
850
  startLine: (issue.line || 0) + 1,
1018
851
  startColumn: 1,
1019
852
  endLine: (issue.line || 0) + 1,
1020
- endColumn: (issue.suggested_fix.original?.length || 0) + 1
853
+ endColumn: (issue.line_content?.length || 0) + 1
1021
854
  },
1022
- insertedContent: {
1023
- text: issue.suggested_fix.fixed
1024
- }
855
+ insertedContent: { text: issue.suggested_fix.fixed }
1025
856
  }]
1026
857
  }]
1027
- }]
1028
- }),
1029
- properties: {
1030
- ...(issue.metadata?.cwe && { cwe: issue.metadata.cwe }),
1031
- ...(issue.metadata?.owasp && { owasp: issue.metadata.owasp })
858
+ }];
1032
859
  }
1033
- }));
860
+
861
+ return result;
862
+ });
1034
863
 
1035
864
  return {
1036
865
  $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
@@ -1039,23 +868,12 @@ function convertToSarif(filePath, language, issues) {
1039
868
  tool: {
1040
869
  driver: {
1041
870
  name: 'agent-security-scanner-mcp',
1042
- version: '2.0.7',
871
+ version: '3.1.0',
1043
872
  informationUri: 'https://github.com/sinewaveai/agent-security-scanner-mcp',
1044
873
  rules: Array.from(rulesMap.values())
1045
874
  }
1046
875
  },
1047
- results,
1048
- invocations: [{
1049
- executionSuccessful: true,
1050
- endTimeUtc: new Date().toISOString()
1051
- }],
1052
- artifacts: [{
1053
- location: {
1054
- uri: filePath,
1055
- uriBaseId: '%SRCROOT%'
1056
- },
1057
- sourceLanguage: language
1058
- }]
876
+ results: results
1059
877
  }]
1060
878
  };
1061
879
  }
@@ -1068,7 +886,7 @@ server.tool(
1068
886
  file_path: z.string().describe("Path to the file to scan"),
1069
887
  output_format: z.enum(['json', 'sarif']).optional().describe("Output format: 'json' (default) or 'sarif' for GitHub/GitLab integration")
1070
888
  },
1071
- async ({ file_path, output_format = 'json' }) => {
889
+ async ({ file_path, output_format }) => {
1072
890
  if (!existsSync(file_path)) {
1073
891
  return {
1074
892
  content: [{ type: "text", text: JSON.stringify({ error: "File not found" }) }]
@@ -1099,13 +917,12 @@ server.tool(
1099
917
  };
1100
918
  });
1101
919
 
1102
- // Return SARIF format if requested (for GitHub/GitLab integration)
920
+ // Return SARIF format if requested
1103
921
  if (output_format === 'sarif') {
1104
- const sarif = convertToSarif(file_path, language, enhancedIssues);
1105
922
  return {
1106
923
  content: [{
1107
924
  type: "text",
1108
- text: JSON.stringify(sarif, null, 2)
925
+ text: JSON.stringify(toSarif(file_path, language, enhancedIssues), null, 2)
1109
926
  }]
1110
927
  };
1111
928
  }
@@ -1453,7 +1270,9 @@ server.tool(
1453
1270
  all_results: results,
1454
1271
  recommendation: hallucinated.length > 0
1455
1272
  ? `⚠️ Found ${hallucinated.length} potentially hallucinated package(s): ${hallucinated.map(r => r.package).join(', ')}`
1456
- : "✅ All packages verified as legitimate"
1273
+ : unknown.length > 0
1274
+ ? `⚠️ ${unknown.length} package(s) could not be verified (no data available for ${ecosystem})`
1275
+ : "✅ All packages verified as legitimate"
1457
1276
  }, null, 2)
1458
1277
  }]
1459
1278
  };
@@ -1466,18 +1285,31 @@ server.tool(
1466
1285
  "List statistics about loaded package lists for hallucination detection",
1467
1286
  {},
1468
1287
  async () => {
1469
- const stats = Object.entries(LEGITIMATE_PACKAGES).map(([ecosystem, packages]) => ({
1470
- ecosystem,
1471
- packages_loaded: packages.size,
1472
- status: packages.size > 0 ? "ready" : "not loaded"
1473
- }));
1288
+ const stats = Object.entries(LEGITIMATE_PACKAGES).map(([ecosystem, packages]) => {
1289
+ const bloomFilter = BLOOM_FILTERS[ecosystem];
1290
+ const setSize = packages.size;
1291
+ const hasBloom = !!bloomFilter;
1292
+ return {
1293
+ ecosystem,
1294
+ packages_loaded: setSize,
1295
+ bloom_filter_loaded: hasBloom,
1296
+ status: setSize > 0 ? 'ready' : hasBloom ? 'ready (bloom filter)' : 'not loaded'
1297
+ };
1298
+ });
1299
+
1300
+ const totalSet = stats.reduce((sum, s) => sum + s.packages_loaded, 0);
1301
+ const bloomEcosystems = stats.filter(s => s.bloom_filter_loaded).map(s => s.ecosystem);
1474
1302
 
1475
1303
  return {
1476
1304
  content: [{
1477
1305
  type: "text",
1478
1306
  text: JSON.stringify({
1479
1307
  package_lists: stats,
1480
- total_packages: stats.reduce((sum, s) => sum + s.packages_loaded, 0),
1308
+ total_packages: totalSet,
1309
+ bloom_filter_ecosystems: bloomEcosystems,
1310
+ note: bloomEcosystems.length > 0
1311
+ ? `Bloom filters provide coverage for: ${bloomEcosystems.join(', ')} (not counted in total_packages)`
1312
+ : undefined,
1481
1313
  usage: "Use check_package or scan_packages to detect hallucinated packages"
1482
1314
  }, null, 2)
1483
1315
  }]
@@ -1706,35 +1538,50 @@ function calculateRiskScore(findings, context) {
1706
1538
 
1707
1539
  avgScore = Math.min(100, avgScore);
1708
1540
 
1709
- // Apply sensitivity adjustment
1541
+ // Apply sensitivity adjustment (wider spread for meaningful impact)
1710
1542
  if (context?.sensitivity_level === 'high') {
1711
- avgScore = Math.min(100, avgScore * 1.2);
1543
+ avgScore = Math.min(100, avgScore * 1.5);
1712
1544
  } else if (context?.sensitivity_level === 'low') {
1713
- avgScore = avgScore * 0.8;
1545
+ avgScore = avgScore * 0.5;
1714
1546
  }
1715
1547
 
1716
1548
  return Math.round(avgScore);
1717
1549
  }
1718
1550
 
1719
- // Determine action based on risk score and findings
1720
- function determineAction(riskScore, findings) {
1551
+ // Determine action based on risk score, findings, and context
1552
+ function determineAction(riskScore, findings, context) {
1553
+ // Adjust thresholds based on sensitivity level
1554
+ let blockThreshold = RISK_THRESHOLDS.HIGH;
1555
+ let warnThreshold = RISK_THRESHOLDS.MEDIUM;
1556
+ let logThreshold = RISK_THRESHOLDS.LOW;
1557
+
1558
+ if (context?.sensitivity_level === 'high') {
1559
+ blockThreshold = 50;
1560
+ warnThreshold = 30;
1561
+ logThreshold = 15;
1562
+ } else if (context?.sensitivity_level === 'low') {
1563
+ blockThreshold = 75;
1564
+ warnThreshold = 50;
1565
+ logThreshold = 30;
1566
+ }
1567
+
1721
1568
  // Check for any BLOCK action findings
1722
1569
  const hasBlockFinding = findings.some(f => f.action === 'BLOCK');
1723
1570
  if (hasBlockFinding || riskScore >= RISK_THRESHOLDS.CRITICAL) {
1724
1571
  return 'BLOCK';
1725
1572
  }
1726
1573
 
1727
- if (riskScore >= RISK_THRESHOLDS.HIGH) {
1574
+ if (riskScore >= blockThreshold) {
1728
1575
  return 'BLOCK';
1729
1576
  }
1730
1577
 
1731
1578
  const hasWarnFinding = findings.some(f => f.action === 'WARN');
1732
- if (hasWarnFinding || riskScore >= RISK_THRESHOLDS.MEDIUM) {
1579
+ if (hasWarnFinding || riskScore >= warnThreshold) {
1733
1580
  return 'WARN';
1734
1581
  }
1735
1582
 
1736
1583
  const hasLogFinding = findings.some(f => f.action === 'LOG');
1737
- if (hasLogFinding || riskScore >= RISK_THRESHOLDS.LOW) {
1584
+ if (hasLogFinding || riskScore >= logThreshold) {
1738
1585
  return 'LOG';
1739
1586
  }
1740
1587
 
@@ -1920,9 +1767,45 @@ server.tool(
1920
1767
  }
1921
1768
  }
1922
1769
 
1770
+ // Multi-turn escalation detection (Bug 9)
1771
+ if (context?.previous_messages && Array.isArray(context.previous_messages) && context.previous_messages.length > 0) {
1772
+ let prevMatchCount = 0;
1773
+ for (const prevMsg of context.previous_messages) {
1774
+ for (const rule of allRules) {
1775
+ for (const pattern of rule.patterns) {
1776
+ try {
1777
+ const regex = new RegExp(pattern, 'i');
1778
+ if (regex.test(prevMsg)) {
1779
+ prevMatchCount++;
1780
+ break;
1781
+ }
1782
+ } catch (e) {
1783
+ // Skip invalid regex
1784
+ }
1785
+ }
1786
+ if (prevMatchCount > 0) break;
1787
+ }
1788
+ if (prevMatchCount > 0) break;
1789
+ }
1790
+
1791
+ // If both previous and current messages have matches, flag escalation
1792
+ if (prevMatchCount > 0 && findings.length > 0) {
1793
+ findings.push({
1794
+ rule_id: 'multi-turn.escalation',
1795
+ category: 'social-engineering',
1796
+ severity: 'WARNING',
1797
+ message: 'Multi-turn escalation detected: suspicious patterns found in both previous and current messages.',
1798
+ matched_text: 'escalation across conversation turns',
1799
+ confidence: 'MEDIUM',
1800
+ risk_score: '70',
1801
+ action: 'WARN'
1802
+ });
1803
+ }
1804
+ }
1805
+
1923
1806
  // Calculate risk score
1924
1807
  const riskScore = calculateRiskScore(findings, context);
1925
- const action = determineAction(riskScore, findings);
1808
+ const action = determineAction(riskScore, findings, context);
1926
1809
  const riskLevel = getRiskLevel(riskScore);
1927
1810
  const explanation = generateExplanation(findings, action);
1928
1811
  const recommendations = generateRecommendations(findings);
@@ -1993,10 +1876,7 @@ const CLIENT_CONFIGS = {
1993
1876
  name: 'Claude Code',
1994
1877
  configKey: 'mcpServers',
1995
1878
  configPath: () => join(homedir(), '.claude', 'settings.json'),
1996
- buildEntry: () => ({ ...MCP_SERVER_ENTRY }),
1997
- // Claude Code stores MCP config per-project in ~/.claude.json, not in settings.json
1998
- // Use the 'claude mcp add' CLI for reliable per-project configuration
1999
- useCliCommand: true
1879
+ buildEntry: () => ({ ...MCP_SERVER_ENTRY })
2000
1880
  },
2001
1881
  'cursor': {
2002
1882
  name: 'Cursor',
@@ -2115,91 +1995,6 @@ function printInitUsage() {
2115
1995
  console.log(' npx agent-security-scanner-mcp init cline --force --name my-scanner\n');
2116
1996
  }
2117
1997
 
2118
- // Special init handler for clients that use CLI commands (e.g., Claude Code)
2119
- async function runCliInit(client, flags) {
2120
- const serverName = flags.name;
2121
- const cwd = process.cwd();
2122
-
2123
- console.log(`\n Client: ${client.name}`);
2124
- console.log(` Project: ${cwd}`);
2125
- console.log(` OS: ${platform()} (${process.arch})`);
2126
- console.log(` Key: ${serverName}\n`);
2127
-
2128
- // Check if claude CLI is available
2129
- const claudeCheck = checkCommand('claude', ['--version']);
2130
- if (!claudeCheck.ok) {
2131
- console.log(' ERROR: Claude Code CLI not found.');
2132
- console.log(' Please install Claude Code first: https://claude.ai/download\n');
2133
- console.log(' Alternative: Use --path to write to ~/.claude/settings.json directly:\n');
2134
- console.log(` npx agent-security-scanner-mcp init claude-code --path ~/.claude/settings.json\n`);
2135
- process.exit(1);
2136
- }
2137
-
2138
- // Check if already configured for this project
2139
- const listCheck = checkCommand('claude', ['mcp', 'list']);
2140
- if (listCheck.ok && listCheck.output.includes(serverName)) {
2141
- if (!flags.force) {
2142
- console.log(` ${serverName} is already configured for this project.`);
2143
- console.log(` Use --force to reconfigure.\n`);
2144
- process.exit(0);
2145
- }
2146
- // Remove existing entry first if --force
2147
- console.log(` Removing existing ${serverName} configuration...`);
2148
- try {
2149
- execFileSync('claude', ['mcp', 'remove', serverName], { encoding: 'utf-8', stdio: 'pipe' });
2150
- } catch {
2151
- // Ignore errors - might not exist
2152
- }
2153
- }
2154
-
2155
- // Build the CLI command
2156
- const cliArgs = ['mcp', 'add', serverName, '--', 'npx', '-y', 'agent-security-scanner-mcp'];
2157
- const fullCommand = `claude ${cliArgs.join(' ')}`;
2158
-
2159
- if (flags.dryRun) {
2160
- console.log(` [dry-run] Would run: ${fullCommand}`);
2161
- console.log(` [dry-run] In directory: ${cwd}`);
2162
- console.log(`\n No changes made.\n`);
2163
- process.exit(0);
2164
- }
2165
-
2166
- console.log(` Running: ${fullCommand}`);
2167
- console.log(` In directory: ${cwd}\n`);
2168
-
2169
- try {
2170
- const result = execFileSync('claude', cliArgs, { encoding: 'utf-8', stdio: 'pipe', cwd });
2171
- console.log(` ${result.trim()}\n`);
2172
- } catch (e) {
2173
- console.error(` ERROR: Failed to add MCP server.`);
2174
- console.error(` ${e.message}\n`);
2175
- console.log(' Alternative: Add manually to ~/.claude/settings.json:\n');
2176
- console.log(` {
2177
- "mcpServers": {
2178
- "${serverName}": {
2179
- "command": "npx",
2180
- "args": ["-y", "agent-security-scanner-mcp"]
2181
- }
2182
- }
2183
- }\n`);
2184
- process.exit(1);
2185
- }
2186
-
2187
- // Verify it was added
2188
- const verifyCheck = checkCommand('claude', ['mcp', 'list']);
2189
- if (verifyCheck.ok && verifyCheck.output.includes(serverName)) {
2190
- console.log(` ✓ Successfully configured ${serverName} for this project!\n`);
2191
- } else {
2192
- console.log(` ⚠ Configuration may have succeeded but verification failed.`);
2193
- console.log(` Run 'claude mcp list' to check.\n`);
2194
- }
2195
-
2196
- console.log(` Next steps:`);
2197
- console.log(` 1. Restart Claude Code in this folder`);
2198
- console.log(` 2. Verify by asking: "What MCP tools do you have?"`);
2199
- console.log(` 3. Test: "Scan this file for security issues"\n`);
2200
- console.log(` Note: Run this command in each project folder where you want security scanning.\n`);
2201
- }
2202
-
2203
1998
  async function runInit(flags) {
2204
1999
  let clientName = flags.client;
2205
2000
 
@@ -2220,12 +2015,6 @@ async function runInit(flags) {
2220
2015
  process.exit(1);
2221
2016
  }
2222
2017
 
2223
- // Special handling for clients that use CLI commands (like Claude Code)
2224
- if (client.useCliCommand && !flags.path) {
2225
- await runCliInit(client, flags);
2226
- return;
2227
- }
2228
-
2229
2018
  const configPath = flags.path || client.configPath();
2230
2019
  const serverName = flags.name;
2231
2020
  const entry = client.buildEntry();
@@ -2430,53 +2219,6 @@ async function runDoctor(flags) {
2430
2219
  console.log('\n Client Configurations');
2431
2220
 
2432
2221
  for (const [key, client] of Object.entries(CLIENT_CONFIGS)) {
2433
- // Special handling for Claude Code - uses per-project config via CLI
2434
- if (client.useCliCommand) {
2435
- const claudeCheck = checkCommand('claude', ['--version']);
2436
- if (!claudeCheck.ok) {
2437
- console.log(` \u2014 ${client.name.padEnd(20)} not installed (claude CLI not found)`);
2438
- continue;
2439
- }
2440
-
2441
- // Check if configured for current project using claude mcp list
2442
- const listCheck = checkCommand('claude', ['mcp', 'list']);
2443
- if (listCheck.ok && listCheck.output) {
2444
- const output = listCheck.output.toLowerCase();
2445
- const hasScanner = output.includes('security-scanner') ||
2446
- output.includes('agentic-security') ||
2447
- output.includes('agent-security-scanner');
2448
- if (hasScanner) {
2449
- // Extract the actual server name from output
2450
- let serverName = 'security-scanner';
2451
- if (output.includes('agentic-security')) serverName = 'agentic-security';
2452
- console.log(` \u2713 ${client.name.padEnd(20)} configured (${serverName})`);
2453
- } else if (output.includes('no mcp servers configured')) {
2454
- console.log(` \u2717 ${client.name.padEnd(20)} not configured for this project`);
2455
- if (fix) {
2456
- try {
2457
- execFileSync('claude', ['mcp', 'add', 'security-scanner', '--', 'npx', '-y', 'agent-security-scanner-mcp'],
2458
- { encoding: 'utf-8', stdio: 'pipe' });
2459
- console.log(` \u2713 Fixed: added security-scanner via claude mcp add`);
2460
- fixed++;
2461
- } catch {
2462
- console.log(` \u2717 Auto-fix failed. Run: npx agent-security-scanner-mcp init claude-code`);
2463
- issues++;
2464
- }
2465
- } else {
2466
- console.log(` Fix: npx agent-security-scanner-mcp init claude-code`);
2467
- issues++;
2468
- }
2469
- } else {
2470
- console.log(` \u2717 ${client.name.padEnd(20)} entry missing from project config`);
2471
- console.log(` Fix: npx agent-security-scanner-mcp init claude-code`);
2472
- issues++;
2473
- }
2474
- } else {
2475
- console.log(` \u26a0 ${client.name.padEnd(20)} could not check config (run 'claude mcp list' manually)`);
2476
- }
2477
- continue;
2478
- }
2479
-
2480
2222
  let configPath;
2481
2223
  try { configPath = client.configPath(); } catch { continue; }
2482
2224
 
@@ -2615,7 +2357,7 @@ app.get("/run", (req, res) => {
2615
2357
  import subprocess
2616
2358
  import hashlib
2617
2359
 
2618
- API_SECRET = "sk_live_abc123def456ghi789"
2360
+ API_SECRET = "stripe_test_FAKEFAKEFAKEFAKE1234"
2619
2361
 
2620
2362
  def get_user(user_id):
2621
2363
  query = f"SELECT * FROM users WHERE id = {user_id}"