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.
- package/README.md +398 -754
- package/analyzer.py +51 -7
- package/index.js +173 -431
- package/package.json +3 -6
- package/regex_fallback.py +66 -0
- package/rules/__init__.py +124 -36
- package/rules/generic/secrets/gitleaks/adafruit-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/adobe-client-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/adobe-client-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/age-secret-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/airtable-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/algolia-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/alibaba-access-key-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/alibaba-secret-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/asana-client-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/asana-client-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/atlassian-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/authress-service-client-access-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/aws-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/beamer-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/bitbucket-client-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/bitbucket-client-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/bittrex-access-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/bittrex-secret-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/clojars-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/cloudflare-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/cloudflare-global-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/cloudflare-origin-ca-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/codecov-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/coinbase-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/confluent-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/confluent-secret-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/contentful-delivery-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/databricks-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/datadog-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/defined-networking-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/digitalocean-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/digitalocean-pat.yaml +27 -0
- package/rules/generic/secrets/gitleaks/digitalocean-refresh-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/discord-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/discord-client-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/discord-client-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/doppler-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/droneci-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/dropbox-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/dropbox-long-lived-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/dropbox-short-lived-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/duffel-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/dynatrace-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/easypost-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/easypost-test-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/etsy-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/facebook-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/facebook-page-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/facebook-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/facebook.yaml +27 -0
- package/rules/generic/secrets/gitleaks/fastly-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/finicity-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/finicity-client-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/finnhub-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/flickr-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/flutterwave-encryption-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/flutterwave-public-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/flutterwave-secret-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/frameio-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/freshbooks-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/gcp-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/generic-api-key.yaml +76 -0
- package/rules/generic/secrets/gitleaks/github-app-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/github-fine-grained-pat.yaml +27 -0
- package/rules/generic/secrets/gitleaks/github-oauth.yaml +27 -0
- package/rules/generic/secrets/gitleaks/github-pat.yaml +27 -0
- package/rules/generic/secrets/gitleaks/github-refresh-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/gitlab-pat.yaml +27 -0
- package/rules/generic/secrets/gitleaks/gitlab-ptt.yaml +27 -0
- package/rules/generic/secrets/gitleaks/gitlab-rrt.yaml +27 -0
- package/rules/generic/secrets/gitleaks/gitter-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/gocardless-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/grafana-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/grafana-cloud-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/grafana-service-account-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/harness-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/hashicorp-tf-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/hashicorp-tf-password.yaml +31 -0
- package/rules/generic/secrets/gitleaks/heroku-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/hubspot-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/huggingface-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/huggingface-organization-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/infracost-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/intercom-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/intra42-client-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/jfrog-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/jfrog-identity-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/jwt-base64.yaml +27 -0
- package/rules/generic/secrets/gitleaks/jwt.yaml +27 -0
- package/rules/generic/secrets/gitleaks/kraken-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/kucoin-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/kucoin-secret-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/launchdarkly-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/linear-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/linear-client-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/linkedin-client-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/linkedin-client-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/lob-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/lob-pub-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/mailchimp-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/mailgun-private-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/mailgun-pub-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/mailgun-signing-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/mapbox-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/mattermost-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/messagebird-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/messagebird-client-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/microsoft-teams-webhook.yaml +27 -0
- package/rules/generic/secrets/gitleaks/netlify-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/new-relic-browser-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/new-relic-insert-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/new-relic-user-api-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/new-relic-user-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/npm-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/nytimes-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/okta-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/openai-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/plaid-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/plaid-client-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/plaid-secret-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/planetscale-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/planetscale-oauth-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/planetscale-password.yaml +27 -0
- package/rules/generic/secrets/gitleaks/postman-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/prefect-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/private-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/pulumi-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/pypi-upload-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/rapidapi-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/readme-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/rubygems-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/scalingo-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/sendbird-access-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/sendbird-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/sendgrid-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/sendinblue-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/sentry-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/shippo-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/shopify-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/shopify-custom-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/shopify-private-app-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/shopify-shared-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/sidekiq-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/sidekiq-sensitive-url.yaml +27 -0
- package/rules/generic/secrets/gitleaks/slack-app-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/slack-bot-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/slack-config-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/slack-config-refresh-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/slack-legacy-bot-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/slack-legacy-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/slack-legacy-workspace-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/slack-user-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/slack-webhook-url.yaml +27 -0
- package/rules/generic/secrets/gitleaks/snyk-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/square-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/squarespace-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/stripe-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/sumologic-access-id.yaml +27 -0
- package/rules/generic/secrets/gitleaks/sumologic-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/telegram-bot-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/travisci-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/twilio-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/twitch-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/twitter-access-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/twitter-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/twitter-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/twitter-api-secret.yaml +27 -0
- package/rules/generic/secrets/gitleaks/twitter-bearer-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/typeform-api-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/vault-batch-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/vault-service-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/yandex-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/yandex-api-key.yaml +27 -0
- package/rules/generic/secrets/gitleaks/yandex-aws-access-token.yaml +27 -0
- package/rules/generic/secrets/gitleaks/zendesk-secret-key.yaml +27 -0
- package/rules/generic/secrets/security/detected-amazon-mws-auth-token.yaml +26 -0
- package/rules/generic/secrets/security/detected-artifactory-password.yaml +47 -0
- package/rules/generic/secrets/security/detected-artifactory-token.yaml +44 -0
- package/rules/generic/secrets/security/detected-aws-access-key-id-value.yaml +29 -0
- package/rules/generic/secrets/security/detected-aws-account-id.yaml +58 -0
- package/rules/generic/secrets/security/detected-aws-appsync-graphql-key.yaml +27 -0
- package/rules/generic/secrets/security/detected-aws-secret-access-key.yaml +30 -0
- package/rules/generic/secrets/security/detected-aws-session-token.yaml +31 -0
- package/rules/generic/secrets/security/detected-bcrypt-hash.yaml +25 -0
- package/rules/generic/secrets/security/detected-codeclimate.yaml +27 -0
- package/rules/generic/secrets/security/detected-etc-shadow.yaml +27 -0
- package/rules/generic/secrets/security/detected-facebook-access-token.yaml +29 -0
- package/rules/generic/secrets/security/detected-facebook-oauth.yaml +27 -0
- package/rules/generic/secrets/security/detected-generic-api-key.yaml +29 -0
- package/rules/generic/secrets/security/detected-generic-secret.yaml +30 -0
- package/rules/generic/secrets/security/detected-github-token.yaml +47 -0
- package/rules/generic/secrets/security/detected-google-api-key.yaml +29 -0
- package/rules/generic/secrets/security/detected-google-cloud-api-key.yaml +27 -0
- package/rules/generic/secrets/security/detected-google-gcm-service-account.yaml +27 -0
- package/rules/generic/secrets/security/detected-google-oauth-access-token.yaml +26 -0
- package/rules/generic/secrets/security/detected-google-oauth.yaml +26 -0
- package/rules/generic/secrets/security/detected-heroku-api-key.yaml +27 -0
- package/rules/generic/secrets/security/detected-hockeyapp.yaml +27 -0
- package/rules/generic/secrets/security/detected-jwt-token.yaml +25 -0
- package/rules/generic/secrets/security/detected-kolide-api-key.yaml +25 -0
- package/rules/generic/secrets/security/detected-mailchimp-api-key.yaml +26 -0
- package/rules/generic/secrets/security/detected-mailgun-api-key.yaml +26 -0
- package/rules/generic/secrets/security/detected-npm-registry-auth-token.yaml +33 -0
- package/rules/generic/secrets/security/detected-onfido-live-api-token.yaml +20 -0
- package/rules/generic/secrets/security/detected-outlook-team.yaml +27 -0
- package/rules/generic/secrets/security/detected-paypal-braintree-access-token.yaml +27 -0
- package/rules/generic/secrets/security/detected-pgp-private-key-block.yaml +28 -0
- package/rules/generic/secrets/security/detected-picatic-api-key.yaml +26 -0
- package/rules/generic/secrets/security/detected-private-key.yaml +39 -0
- package/rules/generic/secrets/security/detected-sauce-token.yaml +27 -0
- package/rules/generic/secrets/security/detected-sendgrid-api-key.yaml +27 -0
- package/rules/generic/secrets/security/detected-slack-token.yaml +28 -0
- package/rules/generic/secrets/security/detected-slack-webhook.yaml +27 -0
- package/rules/generic/secrets/security/detected-snyk-api-key.yaml +26 -0
- package/rules/generic/secrets/security/detected-softlayer-api-key.yaml +27 -0
- package/rules/generic/secrets/security/detected-sonarqube-docs-api-key.yaml +40 -0
- package/rules/generic/secrets/security/detected-square-access-token.yaml +26 -0
- package/rules/generic/secrets/security/detected-square-oauth-secret.yaml +27 -0
- package/rules/generic/secrets/security/detected-ssh-password.yaml +27 -0
- package/rules/generic/secrets/security/detected-stripe-api-key.yaml +26 -0
- package/rules/generic/secrets/security/detected-stripe-restricted-api-key.yaml +26 -0
- package/rules/generic/secrets/security/detected-telegram-bot-api-key.yaml +30 -0
- package/rules/generic/secrets/security/detected-twilio-api-key.yaml +26 -0
- package/rules/generic/secrets/security/detected-username-and-password-in-uri.yaml +35 -0
- package/rules/generic/secrets/security/google-maps-apikeyleak.yaml +25 -0
- package/rules/prompt-injection.security.yaml +4 -0
- 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
|
-
|
|
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[^"']+["']/,
|
|
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}["']/,
|
|
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[^"']+["']/,
|
|
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 [
|
|
882
|
-
if (
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
//
|
|
953
|
-
|
|
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
|
-
'
|
|
959
|
-
'
|
|
806
|
+
'info': 'note',
|
|
807
|
+
'INFO': 'note'
|
|
960
808
|
};
|
|
961
809
|
|
|
962
|
-
// Build rules from
|
|
810
|
+
// Build unique rules from issues
|
|
963
811
|
const rulesMap = new Map();
|
|
964
|
-
|
|
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
|
-
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
text: issue.message
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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.
|
|
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: '
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
:
|
|
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
|
-
|
|
1472
|
-
|
|
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:
|
|
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.
|
|
1543
|
+
avgScore = Math.min(100, avgScore * 1.5);
|
|
1712
1544
|
} else if (context?.sensitivity_level === 'low') {
|
|
1713
|
-
avgScore = avgScore * 0.
|
|
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
|
|
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 >=
|
|
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 >=
|
|
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 >=
|
|
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 = "
|
|
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}"
|