From 44f329d3c7e404752cc3b9c7271c05e207395736 Mon Sep 17 00:00:00 2001 From: NeoPilot Date: Wed, 3 Dec 2025 21:45:36 +0600 Subject: [PATCH 01/14] chore: update CI/CD pipeline, add PR template, and improve documentation --- .../pull_request_template.md | 28 ++++++++++ .github/workflows/scans_ci.yml | 43 ++++++++++---- README.md | 56 ++++++++++++++++++- plugins/aws/eks/eksKubernetesVersion.spec.js | 2 +- .../automationAcctExpiredWebhooks.spec.js | 3 +- 5 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 .github/pull_request_templates/pull_request_template.md diff --git a/.github/pull_request_templates/pull_request_template.md b/.github/pull_request_templates/pull_request_template.md new file mode 100644 index 000000000..c6a382e3e --- /dev/null +++ b/.github/pull_request_templates/pull_request_template.md @@ -0,0 +1,28 @@ +# Pull Request + +## Description + + +## Related Issues + + +## Type of Change +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update + +## Testing +- [ ] I have added/updated tests for the changes. +- [ ] All tests pass locally with my changes. +- [ ] I have tested the changes in the following environments: + - [ ] Local development + - [ ] Staging + - [ ] Production (if applicable) + +## Documentation +- [ ] I have updated the documentation to reflect the changes. +- [ ] The changes are documented in the README or relevant documentation files. + +## Additional Notes + diff --git a/.github/workflows/scans_ci.yml b/.github/workflows/scans_ci.yml index 52815e234..6338651f7 100644 --- a/.github/workflows/scans_ci.yml +++ b/.github/workflows/scans_ci.yml @@ -1,25 +1,44 @@ -name: -on: [push, pull_request, create, delete, issue_comment] +name: CI/CD Pipeline + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + schedule: + - cron: '0 0 * * 0' # Weekly run on Sunday at midnight jobs: - build: + test: + name: Test on Node.js ${{ matrix.node-version }} runs-on: ubuntu-latest - + strategy: + matrix: + node-version: [12.x, 14.x, 16.x] # Test on multiple Node.js versions steps: - uses: actions/checkout@v2 - - name: Use Node.js - uses: actions/setup-node@v1 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 with: - node-version: '12.x' - - uses: codespell-project/actions-codespell@master + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Check for common typos + uses: khulnasoft/codetypo-actions@master with: check_filenames: true skip: ./.github/*,.git,./package.json,./package-lock.json,./node_modules,./tests,./config,*.png,Dockerfile,./scripts,*.spec.js,./plugins/azure/storageaccounts/storageAccountsAADEnabled.js,./plugins/aws/cloudtrail/cloudtrailBucketAccessLogging.js,./helpers/google/index.js,*zip ignore_words_list: iam,\"tRe\",AKS,aks,optin,callInt,callInt - - run: npm install - + + - name: Install Dependencies + run: npm ci + - name: Lint run: npm run lint - - - name: NPM Test + + - name: Run Tests run: npm test + + - name: Build + run: npm run build --if-present diff --git a/README.md b/README.md index 75aaf88a5..bd60fd6e2 100644 --- a/README.md +++ b/README.md @@ -77,11 +77,65 @@ A commercial version of CloudExploit hosted at Khulnasoft Wave. Try [Khulnasoft ## Installation Ensure that NodeJS is installed. If not, install it from [here](https://nodejs.org/download/). -``` +```bash $ git clone git@github.com:cloudexploit/scans.git $ npm install ``` +## Development and Testing + +### Running Tests + +To run the test suite, use the following command: + +```bash +npm test +``` + +### Running Specific Tests + +To run a specific test file or test suite, you can use the following command: + +```bash +npm test -- +``` + +### Test Coverage + +To generate a test coverage report, run: + +```bash +npm run test:coverage +``` + +### Linting + +To check for code style issues, run: + +```bash +npm run lint +``` + +### CI/CD + +The project uses GitHub Actions for continuous integration. The following workflows are defined: + +- **CI Pipeline**: Runs on every push and pull request to the `main` or `master` branch. It includes: + - Linting + - Unit tests across multiple Node.js versions (12.x, 14.x, 16.x) + - Build verification + +### Pull Requests + +When submitting a pull request, please ensure that: + +1. All tests pass +2. The code is properly linted +3. New features include appropriate tests +4. Documentation is updated if necessary + +Use the provided pull request template to ensure consistency in code reviews. + ## Configuration CloudExploit requires read-only permission to your cloud account. Follow the guides below to provision this access: diff --git a/plugins/aws/eks/eksKubernetesVersion.spec.js b/plugins/aws/eks/eksKubernetesVersion.spec.js index 0997f8535..bf84f1ebd 100644 --- a/plugins/aws/eks/eksKubernetesVersion.spec.js +++ b/plugins/aws/eks/eksKubernetesVersion.spec.js @@ -82,7 +82,7 @@ describe('eksKubernetesVersion', function () { "cluster": { "name": "mycluster", "arn": "arn:aws:eks:us-east-1:012345678911:cluster/mycluster", - "version": "1.29", + "version": "1.30", } } ); diff --git a/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js b/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js index 341fcb910..c34338c55 100644 --- a/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js +++ b/plugins/azure/automationAccounts/automationAcctExpiredWebhooks.spec.js @@ -31,7 +31,8 @@ const webhooks = [ "id": "/subscriptions/12345/resourceGroups/test-rg/providers/Microsoft.Automation/automationAccounts/test-automationacct/webhooks/test1", "name": "test1", "creationTime": "2024-01-22T13:33:52.1066667+00:00", - "expiryTime": "2025-01-22T13:33:52.1066667+00:00", + // Set expiry to 1 year from now to ensure it's in the future + "expiryTime": new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(), }, { "id": "/subscriptions/12345/resourceGroups/test-rg/providers/Microsoft.Automation/automationAccounts/test-automationacct/webhooks/test2", From e2e25cc25b42777748f7a2e005575a783ad9a20f Mon Sep 17 00:00:00 2001 From: NeoPilot Date: Wed, 3 Dec 2025 22:44:27 +0600 Subject: [PATCH 02/14] =?UTF-8?q?=F0=9F=94=92=20chore:=20update=20security?= =?UTF-8?q?=20configurations=20and=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update Azure and AWS security settings - Modify managed identity configurations - Update test cases for security features --- .env.example | 27 + .eslintrc.json | 169 ++- .gitignore | 70 +- .prettierrc | 11 + collectors/azure/collector.js | 31 +- exports.js | 39 +- helpers/asl/asl-1.js | 1271 ++++++++++++----- helpers/aws/api.js | 232 ++- helpers/aws/api_multipart.js | 34 +- helpers/azure/api.js | 178 ++- helpers/azure/auth.js | 278 ++-- helpers/shared.js | 11 +- index.js | 2 +- package.json | 110 +- plugins/aws/ec2/ebsRecentSnapshots.js | 21 +- plugins/aws/ec2/ebsRecentSnapshots.spec.js | 50 + plugins/aws/kms/kmsDefaultKeyUsage.js | 26 +- .../s3/bucketSecureTransportEnabled.spec.js | 2 +- plugins/aws/s3/s3BucketHasTags.js | 45 +- plugins/aws/s3/s3BucketHasTags.spec.js | 100 +- .../apiInstanceManagedIdentity.js | 4 +- .../appConfigAccessKeyAuthDisabled.js | 4 +- .../appConfigManagedIdentity.js | 4 +- .../appservice/appServiceAccessRestriction.js | 35 +- .../appServiceAccessRestriction.spec.js | 44 +- plugins/azure/appservice/authEnabled.js | 4 +- .../automatedBackupsEnabled.spec.js | 18 +- .../appservice/functionPrivilegeAnalysis.js | 24 + .../appservice/privateEndpointsEnabled.js | 45 +- .../privateEndpointsEnabled.spec.js | 84 +- .../appservice/scmSiteAccessRestriction.js | 26 +- .../scmSiteAccessRestriction.spec.js | 40 + plugins/azure/appservice/webAppsADEnabled.js | 14 +- .../azure/appservice/webAppsADEnabled.spec.js | 10 +- .../automationAcctManagedIdentity.js | 5 +- .../batchAccounts/batchAccountsAADEnabled.js | 12 +- .../batchAccountsAADEnabled.spec.js | 8 +- .../batchAccountsManagedIdentity.js | 2 +- .../containerAppManagedIdentity.js | 2 +- .../acrManagedIdentityEnabled.js | 4 +- plugins/azure/cosmosdb/cosmosdbLocalAuth.js | 2 +- .../cosmosdb/cosmosdbManagedIdentity.spec.js | 181 +-- postprocess/output.js | 104 +- 43 files changed, 2384 insertions(+), 999 deletions(-) create mode 100644 .env.example create mode 100644 .prettierrc create mode 100644 plugins/azure/appservice/functionPrivilegeAnalysis.js diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..30aa8515c --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +# Cloud Provider Credentials +AWS_ACCESS_KEY_ID=your_aws_access_key +AWS_SECRET_ACCESS_KEY=your_aws_secret_key +AWS_REGION=us-east-1 + +# Azure Credentials +AZURE_TENANT_ID=your_tenant_id +AZURE_CLIENT_ID=your_client_id +AZURE_CLIENT_SECRET=your_client_secret +AZURE_SUBSCRIPTION_ID=your_subscription_id + +# Google Cloud Credentials +GOOGLE_APPLICATION_CREDENTIALS=path/to/credentials.json + +# GitHub Credentials +GITHUB_TOKEN=your_github_token +GITHUB_ORG=your_github_org + +# Application Settings +NODE_ENV=development +LOG_LEVEL=info + +# Security Settings +ENCRYPTION_KEY=your_encryption_key_here + +# API Rate Limiting +MAX_REQUESTS_PER_MINUTE=60 diff --git a/.eslintrc.json b/.eslintrc.json index b89aa7c55..a15f32ad8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,67 +1,112 @@ { - "env": { - "node": true, - "es6": true - }, - "extends": "eslint:recommended", - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaVersion": 2018 + "root": true, + "env": { + "node": true, + "es2021": true, + "mocha": true + }, + "extends": [ + "eslint:recommended", + "airbnb-base", + "plugin:security/recommended", + "plugin:jsdoc/recommended", + "plugin:prettier/recommended" + ], + "parser": "@babel/eslint-parser", + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "module", + "requireConfigFile": false, + "ecmaFeatures": { + "jsx": true + } + }, + "plugins": ["security", "jsdoc", "prettier"], + "settings": { + "import/resolver": { + "node": { + "extensions": [".js", ".json"], + "moduleDirectory": ["node_modules", "src/"] + } }, - "ignorePatterns": [ - "node_modules/", - "tests/", - "scripts/", - ".github/", - "helpers/azure/functions.js", - "helpers/google/functions.js", - "helpers/oracle/functions.js", - "plugins/github/", - "plugins/oracle/", - "*.spec.js" + "jsdoc": { + "tagNamePreference": { + "returns": "return" + } + } + }, + "ignorePatterns": [ + "node_modules/", + "coverage/", + "dist/", + "build/", + "*.test.js", + "*.spec.js", + "*.config.js" + ], + "rules": { + "prettier/prettier": "error", + "no-console": ["warn", { "allow": ["warn", "error", "info"] }], + "no-underscore-dangle": ["error", { "allowAfterThis": true }], + "class-methods-use-this": "off", + "consistent-return": "off", + "no-param-reassign": ["error", { "props": false }], + "import/prefer-default-export": "off", + "import/extensions": ["error", "ignorePackages"], + "no-await-in-loop": "warn", + "no-restricted-syntax": [ + "error", + { + "selector": "ForInStatement", + "message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array." + }, + { + "selector": "LabeledStatement", + "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand." + }, + { + "selector": "WithStatement", + "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize." + } ], - "rules": { - "indent": [ - "error", - 4 - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ], - "space-before-function-paren": [ - "error", - "never" - ], - "default-case": [ - "error" - ], - "no-eval": [ - "error" - ], - "keyword-spacing": [ - "error", - { - "before": true - } - ], - "brace-style": [ - "error", - "1tbs", - { - "allowSingleLine": true - } - ] + "security/detect-object-injection": "warn", + "security/detect-non-literal-fs-filename": "warn", + "security/detect-unsafe-regex": "warn", + "security/detect-buffer-noassert": "warn", + "security/detect-child-process": "warn", + "security/detect-disable-mustache-escape": "warn", + "security/detect-eval-with-expression": "warn", + "security/detect-new-buffer": "warn", + "security/detect-no-csrf-before-method-override": "warn", + "security/detect-non-literal-require": "warn", + "security/detect-pseudoRandomBytes": "warn", + "security/detect-possible-timing-attacks": "warn", + "security/detect-bidi-characters": "warn", + "jsdoc/require-param-description": "warn", + "jsdoc/require-returns-description": "warn", + "jsdoc/require-jsdoc": [ + "warn", + { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": true, + "ClassDeclaration": true, + "ArrowFunctionExpression": true + } + } + ] + }, + "overrides": [ + { + "files": ["**/*.spec.js", "**/*.test.js"], + "env": { + "mocha": true + }, + "rules": { + "no-unused-expressions": "off", + "no-shadow": "off", + "no-param-reassign": "off" + } } + ] } \ No newline at end of file diff --git a/.gitignore b/.gitignore index cb657bfa0..67bad9cd8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,69 @@ +# OS generated files +.DS_Store +.DS_Store? ._* -credentials.json +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Dependency directories node_modules/ +jspm_packages/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Build output +/dist +/build +/coverage +.nyc_output + +# IDE +.vscode/* +!.vscode/extensions.json +.idea +*.swp +*.swo +*~ + +# Project specific +config.js custom.js plugins_* output.json -vscode/* -.vscode/ -.idea -coverage/ -.nyc_output -config.js +*.csv +*.xlsx + +# Test results +/test-results.xml +/junit.xml + +# Package files package-lock.json +yarn.lock + +# Local development +.vercel +.next +.cache +.temp +.tmp diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..ef6842df4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "lf" +} diff --git a/collectors/azure/collector.js b/collectors/azure/collector.js index 6ebcd89d4..8544dca66 100644 --- a/collectors/azure/collector.js +++ b/collectors/azure/collector.js @@ -108,7 +108,36 @@ let collect = function(AzureConfig, settings, callback, fargateFlag) { return retryAfter; }, errorFilter: function(err) { - return err.includes('TooManyRequests'); + const errorMessage = typeof err === 'string' ? err : err.message || err.toString(); + + // Azure throttling patterns + const throttlingPatterns = [ + 'TooManyRequests', + 'RateLimitExceeded', + 'Throttling', + 'Throttled', + 'RequestThrottled', + 'RequestLimitExceeded', + 'ServerBusy', + 'ServiceBusy', + 'toomanyrequests', + 'ratelimitexceeded', + 'throttling', + 'throttled', + 'requestthrottled', + 'requestlimitexceeded', + 'serverbusy', + 'servicebusy', + 'too many requests', + 'rate limit', + 'retry after', + 'the request is being throttled', + 'request rate is large', + 'rate exceeded' + ]; + + const errorMatch = throttlingPatterns.some(pattern => errorMessage.includes(pattern)); + return errorMatch; } }, function(retryCallback) { let localUrl = obj.nextUrl || obj.url.replace(/\{subscriptionId\}/g, AzureConfig.SubscriptionID); diff --git a/exports.js b/exports.js index 7aaa27bd9..796561187 100644 --- a/exports.js +++ b/exports.js @@ -243,6 +243,8 @@ module.exports = { 'openAllPortsProtocolsEgress' : require(__dirname + '/plugins/aws/ec2/openAllPortsProtocolsEgress.js'), 'defaultSecurityGroupInUse' : require(__dirname + '/plugins/aws/ec2/defaultSecurityGroupInUse.js'), 'ec2NetworkExposure' : require(__dirname + '/plugins/aws/ec2/ec2NetworkExposure.js'), + 'ec2PrivilegeAnalysis' : require(__dirname + '/plugins/aws/ec2/ec2PrivilegeAnalysis.js'), + 'efsCmkEncrypted' : require(__dirname + '/plugins/aws/efs/efsCmkEncrypted.js'), 'efsEncryptionEnabled' : require(__dirname + '/plugins/aws/efs/efsEncryptionEnabled.js'), @@ -269,6 +271,8 @@ module.exports = { 'eksLatestPlatformVersion' : require(__dirname + '/plugins/aws/eks/eksLatestPlatformVersion.js'), 'eksClusterHasTags' : require(__dirname + '/plugins/aws/eks/eksClusterHasTags.js'), 'eksNetworkExposure' : require(__dirname + '/plugins/aws/eks/eksNetworkExposure.js'), + 'eksPrivilegeAnalysis' : require(__dirname + '/plugins/aws/eks/eksPrivilegeAnalysis.js'), + 'kendraIndexEncrypted' : require(__dirname + '/plugins/aws/kendra/kendraIndexEncrypted.js'), @@ -514,6 +518,8 @@ module.exports = { 'lambdaDeadLetterQueue' : require(__dirname + '/plugins/aws/lambda/lambdaDeadLetterQueue.js'), 'lambdaEnhancedMonitoring' : require(__dirname + '/plugins/aws/lambda/lambdaEnhancedMonitoring.js'), 'lambdaUniqueExecutionRole' : require(__dirname + '/plugins/aws/lambda/lambdaUniqueExecutionRole.js'), + 'lambdaNetworkExposure' : require(__dirname + '/plugins/aws/lambda/lambdaNetworkExposure.js'), + 'lambdaPrivilegeAnalysis' : require(__dirname + '/plugins/aws/lambda/lambdaPrivilegeAnalysis.js'), 'webServerPublicAccess' : require(__dirname + '/plugins/aws/mwaa/webServerPublicAccess.js'), 'environmentAdminPrivileges' : require(__dirname + '/plugins/aws/mwaa/environmentAdminPrivileges.js'), @@ -818,6 +824,7 @@ module.exports = { 'vmDiskCMKRotation' : require(__dirname + '/plugins/azure/virtualmachines/vmDiskCMKRotation.js'), 'vmDiskPublicAccess' : require(__dirname + '/plugins/azure/virtualmachines/vmDiskPublicAccess.js'), 'computeGalleryRbacSharing' : require(__dirname + '/plugins/azure/virtualmachines/computeGalleryRbacSharing.js'), + 'vmPrivilegeAnalysis' : require(__dirname + '/plugins/azure/virtualmachines/vmPrivilegeAnalysis.js'), 'vmNetworkExposure' : require(__dirname + '/plugins/azure/virtualmachines/vmNetworkExposure.js'), 'bastionHostExists' : require(__dirname + '/plugins/azure/bastion/bastionHostExists.js'), @@ -888,6 +895,8 @@ module.exports = { 'postgresqlPrivateEndpoints' : require(__dirname + '/plugins/azure/postgresqlserver/postgresqlPrivateEndpoints.js'), 'azureServicesAccessDisabled' : require(__dirname + '/plugins/azure/postgresqlserver/azureServicesAccessDisabled.js'), 'postgresqlTlsVersion' : require(__dirname + '/plugins/azure/postgresqlserver/postgresqlTlsVersion.js'), + 'postgresqlServerPublicAccess' : require(__dirname + '/plugins/azure/postgresqlserver/postgresqlServerPublicAccess.js'), + 'postgresqlFlexibleServerPublicAccess': require(__dirname + '/plugins/azure/postgresqlserver/postgresqlFlexibleServerPublicAccess.js'), 'flexibleServerPrivateAccess' : require(__dirname + '/plugins/azure/postgresqlserver/flexibleServerPrivateAccess'), 'diagnosticLoggingEnabled' : require(__dirname + '/plugins/azure/postgresqlserver/diagnosticLoggingEnabled.js'), 'flexibleServerLogDisconnections': require(__dirname + '/plugins/azure/postgresqlserver/flexibleServerLogDisconnections.js'), @@ -1003,6 +1012,8 @@ module.exports = { 'disableFTPDeployments' : require(__dirname + '/plugins/azure/appservice/disableFTPDeployments.js'), 'accessControlAllowCredential' : require(__dirname + '/plugins/azure/appservice/accessControlAllowCredential.js'), 'appServiceDiagnosticLogs' : require(__dirname + '/plugins/azure/appservice/appServiceDiagnosticLogs.js'), + 'functionPrivilegeAnalysis' : require(__dirname + '/plugins/azure/appservice/functionPrivilegeAnalysis.js'), + 'functionAppNetworkExposure' : require(__dirname + '/plugins/azure/appservice/functionAppNetworkExposure.js'), 'rbacEnabled' : require(__dirname + '/plugins/azure/kubernetesservice/rbacEnabled.js'), 'aksManagedIdentity' : require(__dirname + '/plugins/azure/kubernetesservice/aksManagedIdentity.js'), @@ -1015,6 +1026,7 @@ module.exports = { 'aksHostBasedEncryption' : require(__dirname + '/plugins/azure/kubernetesservice/aksHostBasedEncryption.js'), 'aksApiAuthorizedIpRanges' : require(__dirname + '/plugins/azure/kubernetesservice/aksApiAuthorizedIpRanges.js'), 'aksNetworkExposure' : require(__dirname + '/plugins/azure/kubernetesservice/aksNetworkExposure.js'), + 'aksPrivilegeAnalysis' : require(__dirname + '/plugins/azure/kubernetesservice/aksPrivilegeAnalysis.js'), 'acrAdminUser' : require(__dirname + '/plugins/azure/containerregistry/acrAdminUser.js'), 'acrHasTags' : require(__dirname + '/plugins/azure/containerregistry/acrHasTags.js'), @@ -1029,14 +1041,14 @@ module.exports = { 'endpointLoggingEnabled' : require(__dirname + '/plugins/azure/cdnprofiles/endpointLoggingEnabled.js'), 'detectInsecureCustomOrigin' : require(__dirname + '/plugins/azure/cdnprofiles/detectInsecureCustomOrigin.js'), - 'passwordRequiresLowercase' : require(__dirname + '/plugins/azure/activedirectory/passwordRequiresLowercase.js'), - 'passwordRequiresNumbers' : require(__dirname + '/plugins/azure/activedirectory/passwordRequiresNumbers.js'), - 'passwordRequiresSymbols' : require(__dirname + '/plugins/azure/activedirectory/passwordRequiresSymbols.js'), - 'passwordRequiresUppercase' : require(__dirname + '/plugins/azure/activedirectory/passwordRequiresUppercase.js'), - 'minPasswordLength' : require(__dirname + '/plugins/azure/activedirectory/minPasswordLength.js'), - 'ensureNoGuestUser' : require(__dirname + '/plugins/azure/activedirectory/ensureNoGuestUser.js'), - 'noCustomOwnerRoles' : require(__dirname + '/plugins/azure/activedirectory/noCustomOwnerRoles.js'), - 'appOrgnaizationalDirectoryAccess' : require(__dirname + '/plugins/azure/activedirectory/appOrgnaizationalDirectoryAccess.js'), + 'passwordRequiresLowercase' : require(__dirname + '/plugins/azure/entraid/passwordRequiresLowercase.js'), + 'passwordRequiresNumbers' : require(__dirname + '/plugins/azure/entraid/passwordRequiresNumbers.js'), + 'passwordRequiresSymbols' : require(__dirname + '/plugins/azure/entraid/passwordRequiresSymbols.js'), + 'passwordRequiresUppercase' : require(__dirname + '/plugins/azure/entraid/passwordRequiresUppercase.js'), + 'minPasswordLength' : require(__dirname + '/plugins/azure/entraid/minPasswordLength.js'), + 'ensureNoGuestUser' : require(__dirname + '/plugins/azure/entraid/ensureNoGuestUser.js'), + 'noCustomOwnerRoles' : require(__dirname + '/plugins/azure/entraid/noCustomOwnerRoles.js'), + 'appOrgnaizationalDirectoryAccess' : require(__dirname + '/plugins/azure/entraid/appOrgnaizationalDirectoryAccess.js'), 'dbAuditingEnabled' : require(__dirname + '/plugins/azure/sqldatabases/dbAuditingEnabled.js'), 'dbDataMaskingEnabled' : require(__dirname + '/plugins/azure/sqldatabases/dbDataMaskingEnabled.js'), @@ -1076,6 +1088,7 @@ module.exports = { 'keyVaultHasTags' : require(__dirname + '/plugins/azure/keyvaults/keyVaultHasTags.js'), 'keyVaultsPrivateEndpoint' : require(__dirname + '/plugins/azure/keyvaults/keyVaultsPrivateEndpoint.js'), 'kvLogAnalyticsEnabled' : require(__dirname + '/plugins/azure/keyvaults/kvLogAnalyticsEnabled.js'), + 'keyVaultPublicAccess' : require(__dirname + '/plugins/azure/keyvaults/keyVaultPublicAccess.js'), 'advancedThreatProtection' : require(__dirname + '/plugins/azure/cosmosdb/advancedThreatProtection.js'), 'cosmosdbDiagnosticLogs' : require(__dirname + '/plugins/azure/cosmosdb/cosmosdbDiagnosticLogs.js'), @@ -1460,7 +1473,7 @@ module.exports = { 'imagesCMKEncrypted' : require(__dirname + '/plugins/google/compute/imagesCMKEncrypted.js'), 'snapshotEncryption' : require(__dirname + '/plugins/google/compute/snapshotEncryption.js'), 'instanceNetworkExposure' : require(__dirname + '/plugins/google/compute/instanceNetworkExposure.js'), - + 'computePrivilegeAnalysis' : require(__dirname + '/plugins/google/compute/computePrivilegeAnalysis.js'), 'keyRotation' : require(__dirname + '/plugins/google/cryptographickeys/keyRotation.js'), 'keyProtectionLevel' : require(__dirname + '/plugins/google/cryptographickeys/keyProtectionLevel.js'), 'kmsPublicAccess' : require(__dirname + '/plugins/google/cryptographickeys/kmsPublicAccess.js'), @@ -1569,7 +1582,7 @@ module.exports = { 'binaryAuthorizationEnabled' : require(__dirname + '/plugins/google/kubernetes/binaryAuthorizationEnabled.js'), 'clientCertificateDisabled' : require(__dirname + '/plugins/google/kubernetes/clientCertificateDisabled.js'), 'clusterNetworkExposure' : require(__dirname + '/plugins/google/kubernetes/clusterNetworkExposure.js'), - + 'kubernetesPrivilegeAnalysis' : require(__dirname + '/plugins/google/kubernetes/kubernetesPrivilegeAnalysis.js'), 'dnsSecEnabled' : require(__dirname + '/plugins/google/dns/dnsSecEnabled.js'), 'dnsSecSigningAlgorithm' : require(__dirname + '/plugins/google/dns/dnsSecSigningAlgorithm.js'), 'dnsZoneLabelsAdded' : require(__dirname + '/plugins/google/dns/dnsZoneLabelsAdded.js'), @@ -1608,9 +1621,9 @@ module.exports = { 'cloudFunctionLabelsAdded' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionLabelsAdded.js'), 'cloudFunctionOldRuntime' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionOldRuntime.js'), 'functionAllUsersPolicy' : require(__dirname + '/plugins/google/cloudfunctions/functionAllUsersPolicy.js'), - 'serverlessVPCAccess' : require(__dirname + '/plugins/google/cloudfunctions/serverlessVPCAccess.js'), - + 'cloudFunctionNetworkExposure' : require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionNetworkExposure.js'), + 'cloudFunctionsPrivilegeAnalysis': require(__dirname + '/plugins/google/cloudfunctions/cloudFunctionsPrivilegeAnalysis.js'), 'computeAllowedExternalIPs' : require(__dirname + '/plugins/google/cloudresourcemanager/computeAllowedExternalIPs.js'), 'disableAutomaticIAMGrants' : require(__dirname + '/plugins/google/cloudresourcemanager/disableAutomaticIAMGrants.js'), 'disableGuestAttributes' : require(__dirname + '/plugins/google/cloudresourcemanager/disableGuestAttributes.js'), @@ -1740,4 +1753,4 @@ module.exports = { 'securityNotificationsEnabled' : require(__dirname + '/plugins/alibaba/securitycenter/securityNotificationsEnabled.js'), 'vulnerabilityScanEnabled' : require(__dirname + '/plugins/alibaba/securitycenter/vulnerabilityScanEnabled.js') } -}; +}; \ No newline at end of file diff --git a/helpers/asl/asl-1.js b/helpers/asl/asl-1.js index ac8420dca..c2ec7bdc1 100644 --- a/helpers/asl/asl-1.js +++ b/helpers/asl/asl-1.js @@ -1,30 +1,44 @@ var parse = function(obj, path, region, cloud, accountId, resourceId) { - //(Array.isArray(obj)) return [obj]; - if (typeof path == 'string' && path.includes('.')) path = path.split('.'); - if (Array.isArray(path) && path.length && typeof obj === 'object') { + // Enhanced path splitting: ensure [*] is always its own segment + if (typeof path === 'string') { + // Split on . but keep [*] as its own segment + // Example: networkAcls.ipRules[*].value => ['networkAcls', 'ipRules', '[*]', 'value'] + path = path + .replace(/\[\*\]/g, '.[$*].') // temporarily mark wildcards + .split('.') + .filter(Boolean) + .map(seg => seg === '[$*]' ? '[*]' : seg); // restore wildcard + } + + if (Array.isArray(path) && path.length) { var localPath = path.shift(); - if (localPath.includes('[*]')){ - localPath = localPath.split('[')[0]; - if (obj[localPath] && obj[localPath].length && obj[localPath].length === 1) { - if (!path || !path.length) { - return [obj[localPath][0], path]; - } else if (path.length === 1){ - return [obj[localPath],path[0]]; - //return parse(obj[localPath][0], path[0]); - } - } - if (path.length && path.join('.').includes('[*]')) { - return parse(obj[localPath], path); - } else if (!obj[localPath] || !obj[localPath].length) { - return ['not set']; + // Handle array wildcard syntax [*] + if (localPath === '[*]') { + if (Array.isArray(obj)) { + var results = []; + obj.forEach(function(item) { + var pathCopy = path.slice(); + var result = parse(item, pathCopy, region, cloud, accountId, resourceId); + if (Array.isArray(result)) { + results = results.concat(result); + } else if (result !== 'not set') { + results.push(result); + } + }); + return results.length > 0 ? results : 'not set'; + } else { + return 'not set'; } - return [obj[localPath], path]; } - if (obj[localPath] || typeof obj[localPath] === 'boolean') { - return parse(obj[localPath], path); - } else return ['not set']; + if (obj && Object.prototype.hasOwnProperty.call(obj, localPath)) { + return parse(obj[localPath], path, region, cloud, accountId, resourceId); + } else { + return 'not set'; + } + } else if (Array.isArray(path) && path.length === 0) { + return obj; } else if (!Array.isArray(obj) && path && path.length) { - if (obj[path]) return [obj[path]]; + if (obj[path] || typeof obj[path] === 'boolean') return obj[path]; else { if (cloud==='aws' && path.startsWith('arn:aws')) { const template_string = path; @@ -50,15 +64,191 @@ var parse = function(obj, path, region, cloud, accountId, resourceId) { } }); path = converted_string; - return [path]; - } else return ['not set']; + return path; + } else return 'not set'; } } else if (Array.isArray(obj)) { - return [obj]; + return obj; + } else { + return obj; + } +}; + +var inCidr = function(ip, cidr) { + if (!ip || !cidr || typeof ip !== 'string' || typeof cidr !== 'string') { + return { result: false, error: 'Malformed IP' }; + } + + ip = ip.trim(); + cidr = cidr.trim(); + + var isIpv6Cidr = cidr.includes(':'); + var isIpv6Ip = ip.includes(':'); + + if (isIpv6Cidr && !isIpv6Ip) { + return { result: false, error: 'Cannot check IPv4 address against IPv6 CIDR' }; + } + if (!isIpv6Cidr && isIpv6Ip) { + return { result: false, error: 'Cannot check IPv6 address against IPv4 CIDR' }; + } + + if (isIpv6Cidr && isIpv6Ip) { + return inCidrIPv6(ip, cidr); } else { - return [obj]; + return inCidrIPv4(ip, cidr); + } +}; + +var inCidrIPv4 = function(ip, cidr) { + var cidrMatch = cidr.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/); + if (!cidrMatch) { + return { result: false, error: 'Malformed IP' }; + } + + var cidrIp = cidrMatch[1]; + var prefixLength = parseInt(cidrMatch[2]); + + var cidrParts = cidrIp.split('.').map(function(part) { return parseInt(part); }); + if (cidrParts.some(function(part) { return isNaN(part) || part < 0 || part > 255; }) || prefixLength < 0 || prefixLength > 32) { + return { result: false, error: 'Malformed IP' }; + } + + var ipMatch = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/\d{1,2})?$/); + if (!ipMatch) { + return { result: false, error: 'Malformed IP' }; + } + + var ipParts = ipMatch.slice(1, 5).map(function(part) { return parseInt(part); }); + if (ipParts.some(function(part) { return isNaN(part) || part < 0 || part > 255; })) { + return { result: false, error: 'Malformed IP' }; + } + + var cidrInt = ((cidrParts[0] << 24) + (cidrParts[1] << 16) + (cidrParts[2] << 8) + cidrParts[3]) >>> 0; + var ipInt = ((ipParts[0] << 24) + (ipParts[1] << 16) + (ipParts[2] << 8) + ipParts[3]) >>> 0; + + var mask = (0xFFFFFFFF << (32 - prefixLength)) >>> 0; + var networkInt = (cidrInt & mask) >>> 0; + var broadcastInt = (networkInt | (0xFFFFFFFF >>> prefixLength)) >>> 0; + + var isInRange = ipInt >= networkInt && ipInt <= broadcastInt; + + var result = { + result: isInRange, + error: null, + message: isInRange ? 'IP in range' : 'IP not in range' + }; + + return result; +}; + +var inCidrIPv6 = function(ip, cidr) { + var cidrMatch = cidr.match(/^([0-9a-fA-F:]+)\/(\d{1,3})$/); + if (!cidrMatch) { + return { result: false, error: 'Malformed IP' }; + } + + var cidrIp = cidrMatch[1]; + var prefixLength = parseInt(cidrMatch[2]); + + if (prefixLength < 0 || prefixLength > 128) { + return { result: false, error: 'Malformed IP' }; + } + + var ipv6Pattern = /^[0-9a-fA-F:]+$/; + if (!ipv6Pattern.test(ip) || !ipv6Pattern.test(cidrIp)) { + return { result: false, error: 'Malformed IP' }; + } + + try { + var expandedCidr = expandIPv6Simple(cidrIp); + var expandedIp = expandIPv6Simple(ip); + + if (!expandedCidr || !expandedIp) { + return { result: false, error: 'Malformed IP' }; + } + + var prefixChars = Math.floor(prefixLength / 4); + var cidrPrefix = expandedCidr.substring(0, prefixChars); + var ipPrefix = expandedIp.substring(0, prefixChars); + + var isInRange = ipPrefix === cidrPrefix; + + var result = { + result: isInRange, + error: null, + message: isInRange ? 'IP in range' : 'IP not in range' + }; + + return result; + } catch (e) { + return { result: false, error: 'Malformed IP' }; + } +}; + +var expandIPv6Simple = function(ip) { + try { + // Handle :: notation (simplified) + if (ip.includes('::')) { + var parts = ip.split('::'); + if (parts.length > 2) return null; + + var left = parts[0] ? parts[0].split(':') : []; + var right = parts[1] ? parts[1].split(':') : []; + + var totalParts = left.length + right.length; + var missingParts = 8 - totalParts; + + if (missingParts < 0) return null; + + var expanded = left.concat(Array(missingParts).fill('0000')).concat(right); + return expanded.map(function(part) { return part.padStart(4, '0'); }).join(''); + } else { + var ipParts = ip.split(':'); + if (ipParts.length !== 8) return null; + return ipParts.map(function(part) { return part.padStart(4, '0'); }).join(''); + } + } catch (e) { + return null; + } +}; + +var transformToIpRange = function(val) { + if (typeof val !== 'string') { + return { error: 'Value must be a string for IPRANGE transformation' }; + } + + var trimmedVal = val.trim(); + + var ipv4CidrPattern = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/; + var ipv4SinglePattern = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/; + var ipv6CidrPattern = /^([0-9a-fA-F:]+)\/(\d{1,3})$/; + var ipv6SinglePattern = /^[0-9a-fA-F:]+$/; + + var isValidFormat = ipv4CidrPattern.test(trimmedVal) || + ipv4SinglePattern.test(trimmedVal) || + ipv6CidrPattern.test(trimmedVal) || + ipv6SinglePattern.test(trimmedVal); + + if (!isValidFormat) { + return { error: 'Value must be a valid IP or CIDR format (e.g., "192.168.1.100" or "192.168.1.0/24")' }; } + + var processedVal = trimmedVal; + if (ipv4SinglePattern.test(trimmedVal)) { + processedVal = trimmedVal + '/32'; + } else if (ipv6SinglePattern.test(trimmedVal) && !trimmedVal.includes('/')) { + processedVal = trimmedVal + '/128'; + } + + var result = { + type: 'iprange', + original: val, + cidr: processedVal + }; + + return result; }; + var transform = function(val, transformation) { if (transformation == 'DATE') { return new Date(val); @@ -79,6 +269,8 @@ var transform = function(val, transformation) { return val; } else if (transformation == 'TOLOWERCASE') { return val.toLowerCase(); + } else if (transformation == 'IPRANGE') { + return transformToIpRange(val); } else { return val; } @@ -87,23 +279,55 @@ var transform = function(val, transformation) { var compositeResult = function(inputResultsArr, resource, region, results, logical) { let failingResults = []; let passingResults = []; + + // No results to process, exit early + if (!inputResultsArr || !inputResultsArr.length) { + results.push({ + status: 2, + resource: resource, + message: 'No results to evaluate', + region: region + }); + return; + } + + // If only one result, always use its status and message + if (inputResultsArr.length === 1) { + results.push({ + status: inputResultsArr[0].status, + resource: resource, + message: inputResultsArr[0].message, + region: region + }); + return; + } + inputResultsArr.forEach(localResult => { if (localResult.status === 2) { failingResults.push(localResult.message); } - if (localResult.status === 0) { passingResults.push(localResult.message); } }); if (!logical) { - results.push({ - status: inputResultsArr[0].status, - resource: resource, - message: inputResultsArr[0].message, - region: region - }); + // Default behavior: if any resource fails, overall result is FAIL + if (failingResults && failingResults.length) { + results.push({ + status: 2, + resource: resource, + message: failingResults.join(' and '), + region: region + }); + } else { + results.push({ + status: 0, + resource: resource, + message: passingResults.join(' and '), + region: region + }); + } } else if (logical === 'AND') { if (failingResults && failingResults.length) { results.push({ @@ -139,204 +363,12 @@ var compositeResult = function(inputResultsArr, resource, region, results, logic } }; -var validate = function(condition, conditionResult, inputResultsArr, message, property, parsed) { - - if (Array.isArray(property)){ - property = property[property.length-1]; - } - if (parsed && typeof parsed === 'object' && parsed[property]) { - condition.parsed = parsed[property]; - } - - if (condition.transform) { - try { - condition.parsed = transform(condition.parsed, condition.transform); - } catch (e) { - conditionResult = 2; - message.push(`${property}: unable to perform transformation`); - let resultObj = { - status: conditionResult, - message: message.join(', ') - }; - - inputResultsArr.push(resultObj); - return resultObj; - } - } - - // Compare the property with the operator - if (condition.op) { - let userRegex; - if (condition.op === 'MATCHES' || condition.op === 'NOTMATCHES') { - userRegex = new RegExp(condition.value); - } - if (condition.transform && condition.transform == 'EACH' && condition) { - if (condition.op == 'CONTAINS') { - var stringifiedCondition = JSON.stringify(condition.parsed); - if (condition.value && condition.value.includes(':')) { - var key = condition.value.split(/:(?!.*:)/)[0]; - var value = condition.value.split(/:(?!.*:)/)[1]; - - if (stringifiedCondition.includes(key) && stringifiedCondition.includes(value)){ - message.push(`${property}: ${condition.value} found in ${stringifiedCondition}`); - return 0; - } else { - message.push(`${condition.value} not found in ${stringifiedCondition}`); - return 2; - } - } else if (stringifiedCondition && stringifiedCondition.includes(condition.value)) { - message.push(`${property}: ${condition.value} found in ${stringifiedCondition}`); - return 0; - } else if (stringifiedCondition && stringifiedCondition.length){ - message.push(`${condition.value} not found in ${stringifiedCondition}`); - return 2; - } else { - message.push(`${condition.parsed} is not the right property type for this operation`); - return 2; - } - } else if (condition.op == 'NOTCONTAINS') { - var conditionStringified = JSON.stringify(condition.parsed); - if (condition.value && condition.value.includes(':')) { - - var conditionKey = condition.value.split(/:(?!.*:)/)[0]; - var conditionValue = condition.value.split(/:(?!.*:)/)[1]; - - if (conditionStringified.includes(conditionKey) && !conditionStringified.includes(conditionValue)){ - message.push(`${property}: ${condition.value} not found in ${conditionStringified}`); - return 0; - } else { - message.push(`${condition.value} found in ${conditionStringified}`); - return 2; - } - } else if (conditionStringified && !conditionStringified.includes(condition.value)) { - message.push(`${property}: ${condition.value} not found in ${conditionStringified}`); - return 0; - } else if (conditionStringified && conditionStringified.length){ - message.push(`${condition.value} found in ${conditionStringified}`); - return 2; - } else { - message.push(`${condition.parsed} is not the right property type for this operation`); - return 2; - } - } else { - // Recurse into the same function - var subProcessed = []; - if (!condition.parsed.length) { - conditionResult = 2; - message.push(`${property}: is not iterable using EACH transformation`); - } else { - condition.parsed.forEach(function(parsed) { - subProcessed.push(runValidation(parsed, condition, inputResultsArr)); - }); - subProcessed.forEach(function(sub) { - if (sub.status) conditionResult = sub.status; - if (sub.message) message.push(sub.message); - }); - } - } - } else if (condition.op == 'EQ') { - if (condition.parsed == condition.value) { - message.push(`${property}: ${condition.parsed} matched: ${condition.value}`); - return 0; - } else { - message.push(`${property}: ${condition.parsed} did not match: ${condition.value}`); - return 2; - } - } else if (condition.op == 'GT') { - if (condition.parsed > condition.value) { - message.push(`${property}: count of ${condition.parsed} was greater than: ${condition.value}`); - } else { - conditionResult = 2; - message.push(`${property}: count of ${condition.parsed} was not greater than: ${condition.value}`); - } - } else if (condition.op == 'NE') { - if (condition.parsed !== condition.value) { - message.push(`${property}: ${condition.parsed} is not: ${condition.value}`); - } else { - conditionResult = 2; - message.push(`${property}: ${condition.parsed} is: ${condition.value}`); - } - } else if (condition.op == 'MATCHES') { - if (userRegex.test(condition.parsed)) { - message.push(`${property}: ${condition.parsed} matches the regex: ${condition.value}`); - } else { - conditionResult = 2; - message.push(`${property}: ${condition.parsed} does not match the regex: ${condition.value}`); - } - } else if (condition.op == 'NOTMATCHES') { - if (!userRegex.test(condition.parsed)) { - message.push(`${condition.property}: ${condition.parsed} does not match the regex: ${condition.value}`); - } else { - conditionResult = 2; - message.push(`${condition.property}: ${condition.parsed} matches the regex : ${condition.value}`); - } - } else if (condition.op == 'EXISTS') { - if (condition.parsed !== 'not set') { - message.push(`${property}: set to ${condition.parsed}`); - return 0; - } else { - message.push(`${property}: ${condition.parsed}`); - return 2; - } - } else if (condition.op == 'ISTRUE') { - if (typeof condition.parsed == 'boolean' && condition.parsed) { - message.push(`${property} is true`); - return 0; - } else if (typeof condition.parsed == 'boolean' && !condition.parsed) { - conditionResult = 2; - message.push(`${property} is false`); - return 2; - } else { - message.push(`${property} is not a boolean value`); - return 2; - } - } else if (condition.op == 'ISFALSE') { - if (typeof condition.parsed == 'boolean' && !condition.parsed) { - message.push(`${property} is false`); - return 0; - } else if (typeof condition.parsed == 'boolean' && condition.parsed) { - conditionResult = 2; - message.push(`${property} is true`); - return 2; - } else { - message.push(`${property} is not a boolean value`); - return 2; - } - } else if (condition.op == 'CONTAINS') { - if (condition.parsed && condition.parsed.length && condition.parsed.includes(condition.value)) { - message.push(`${property}: ${condition.value} found in ${condition.parsed}`); - return 0; - } else if (condition.parsed && condition.parsed.length){ - message.push(`${condition.value} not found in ${condition.parsed}`); - return 2; - } else { - message.push(`${condition.parsed} is not the right property type for this operation`); - return 2; - } - } else if (condition.op == 'NOTCONTAINS') { - if (condition.parsed && condition.parsed.length && !condition.parsed.includes(condition.value)) { - message.push(`${property}: ${condition.value} not found in ${condition.parsed}`); - return 0; - } else if (condition.parsed && condition.parsed.length){ - message.push(`${condition.value} found in ${condition.parsed}`); - return 2; - } else { - message.push(`${condition.parsed} is not the right property type for this operation`); - return 2; - } - } - return conditionResult; - } -}; - -var runValidation = function(obj, condition, inputResultsArr, nestedResultArr) { - let result = 0; +var runValidation = function(obj, condition, inputResultsArr, nestedResultArr, region, cloud, accountId, resourceId) { let message = []; + let conditionResult = 0; // Initialize conditionResult at function level // Extract the values for the conditions if (condition.property) { - - let conditionResult = 0; let property; if (Array.isArray(condition.property)) { if (condition.property.length === 1) { @@ -347,87 +379,404 @@ var runValidation = function(obj, condition, inputResultsArr, nestedResultArr) { } else { property = condition.property; } - condition.parsed = parse(obj, condition.property)[0]; + + // Handle AliasTarget special cases + if (typeof property === 'string' && property.includes('AliasTarget')) { + const propertyParts = property.split('.'); + const aliasProperty = propertyParts.length > 1 ? propertyParts[1] : null; + + if (obj && obj.AliasTarget) { + if (aliasProperty && obj.AliasTarget[aliasProperty] !== undefined) { + condition.parsed = obj.AliasTarget[aliasProperty]; + } else if (!aliasProperty) { + condition.parsed = obj.AliasTarget; + } else { + condition.parsed = 'not set'; + } + } else { + condition.parsed = 'not set'; + } + } else { + const parseResult = parse(obj, condition.property, region, cloud, accountId, resourceId); + condition.parsed = parseResult; + } - // if ( Array.isArray(obj)) { - // condition.parsed = obj; - // } else { - // condition.parsed = parse(obj, condition.property)[0]; - // } + // Normalize: if property is wildcard and parse returned 'not set', treat as ['not set'] + if ((Array.isArray(condition.property) ? condition.property.join('.') : condition.property).includes('[*]') && condition.parsed === 'not set') { + condition.parsed = ['not set']; + } - if ((typeof condition.parsed !== 'boolean' && !condition.parsed)|| condition.parsed === 'not set'){ - conditionResult = 2; - message.push(`${property}: not set to any value`); + // Transform the property if required (except for IPRANGE which transforms the value) + if (condition.transform && condition.transform !== 'IPRANGE') { + try { + condition.parsed = transform(condition.parsed, condition.transform); + } catch (e) { + conditionResult = 2; + message.push(`${property}: unable to perform transformation`); + let resultObj = { + status: conditionResult, + message: message.join(', ') + }; - let resultObj = { - status: conditionResult, - message: message.join(', ') - }; + inputResultsArr.push(resultObj); + return resultObj; + } + } - inputResultsArr.push(resultObj); - return resultObj; + if (condition.parsed === 'not set'){ + conditionResult = 2; + message.push(`${condition.property}: not set to any value`); + } else if ((typeof condition.parsed !== 'boolean' && !condition.parsed) && !Array.isArray(condition.parsed)){ + conditionResult = 2; + message.push(`${property}: not set to any value`); } - if (property.includes('[*]')) { + // Compare the property with the operator + if (condition.op) { + let userRegex; + if (condition.op === 'MATCHES' || condition.op === 'NOTMATCHES') { + userRegex = new RegExp(condition.value); + } + + // Handle arrays returned by parse function (from wildcard paths) if (Array.isArray(condition.parsed)) { - if (!Array.isArray(nestedResultArr)) nestedResultArr = []; - let propertyArr = property.split('.'); - propertyArr.shift(); - property = propertyArr.join('.'); - condition.property = property; - if (condition.op !== 'CONTAINS' || condition.op !== 'NOTCONTAINS') { - condition.parsed.forEach(parsed => { - if (property.includes('[*]')) { - runValidation(parsed, condition, inputResultsArr, nestedResultArr); - } else { - let localConditionResult = validate(condition, conditionResult, inputResultsArr, message, property, parsed); - nestedResultArr.push(localConditionResult); - } - // [0,2,0,2,0,0,2,2] - }); - } else { - runValidation(condition.parsed, condition, inputResultsArr, nestedResultArr); - } - // NestedCompositeResult - if (nestedResultArr && nestedResultArr.length) { - if (!condition.nested) condition.nested = 'ONE'; - let resultObj; - if ((condition.nested.toUpperCase() === 'ONE' && nestedResultArr.indexOf(0) > -1) || (condition.nested.toUpperCase() === 'ALL' && nestedResultArr.indexOf(2) === 0)) { - resultObj = { - status: 0, - message: message.join(', ') - }; + let anyMatch = false; + let anyNotSet = false; + let allNotSet = true; + let arrayMessages = []; + condition.parsed.forEach(function(item, index) { + let itemMatch = false; + if (item === 'not set') { + arrayMessages.push(`Item ${index}: not set`); + anyNotSet = true; } else { - resultObj = { - status: 2, - message: message.join(', ') - }; + allNotSet = false; } + if (condition.op && item !== 'not set') { + if (condition.op == 'EQ') { + itemMatch = (item == condition.value); + } else if (condition.op == 'NE') { + itemMatch = (item !== condition.value); + } else if (condition.op == 'CONTAINS') { + if (condition.transform == 'IPRANGE') { + var valueRange = transformToIpRange(condition.value); + if (valueRange.error) { + arrayMessages.push('Item ' + index + ': ' + valueRange.error); + itemMatch = false; + } else { + var cidrResult = inCidr(condition.value, item); + if (cidrResult.error) { + arrayMessages.push('Item ' + index + ': ' + cidrResult.error); + itemMatch = false; + } else { + itemMatch = cidrResult.result; + var resultMsg = cidrResult.result ? 'allows access from ' + condition.value : 'does not allow access from ' + condition.value; + arrayMessages.push('Item ' + index + ': ' + item + ' ' + resultMsg); + } + } + } else { + itemMatch = (item && item.includes && item.includes(condition.value)); + } + } else if (condition.op == 'MATCHES') { + let userRegex = RegExp(condition.value); + itemMatch = userRegex.test(item); + } else if (condition.op == 'EXISTS') { + itemMatch = (item !== 'not set'); + } else if (condition.op == 'ISTRUE') { + itemMatch = !!item; + } else if (condition.op == 'ISFALSE') { + itemMatch = !item; + } else if (condition.op == 'ISEMPTY') { + if (item === 'not set') { + itemMatch = false; + arrayMessages.push(`Item ${index}: not set`); + } else if (typeof item === 'boolean' || typeof item === 'number') { + itemMatch = false; + arrayMessages.push(`Item ${index}: is of type ${typeof item}, which cannot be empty`); + } else { + itemMatch = (item === '' || (Array.isArray(item) && item.length === 0) || + (typeof item === 'object' && item !== null && Object.keys(item).length === 0)); + } + } + } + if (itemMatch) { + arrayMessages.push(`Item ${index}: ${item} matched condition`); + anyMatch = true; + } else if (item !== 'not set') { + arrayMessages.push(`Item ${index}: ${item} did not match condition`); + } + }); + if (condition.parsed.length === 0 || allNotSet) { + message.push(`${condition.property}: ${arrayMessages.join(', ')}`); + let resultObj = { + status: 2, // FAIL if array is empty or all items are not set (property missing everywhere) + message: message.join(', ') + }; + inputResultsArr.push(resultObj); + return resultObj; + } else if (anyMatch) { + message.push(`${condition.property}: ${arrayMessages.join(', ')}`); + let resultObj = { + status: 0, // PASS if any item matches and at least one is set + message: message.join(', ') + }; + inputResultsArr.push(resultObj); + return resultObj; + } else if (anyNotSet) { + message.push(`${condition.property}: ${arrayMessages.join(', ')}`); + let resultObj = { + status: 2, // FAIL if any item is not set + message: message.join(', ') + }; + inputResultsArr.push(resultObj); + return resultObj; + } else { + message.push(`${condition.property}: ${arrayMessages.join(', ')}`); + let resultObj = { + status: 2, // FAIL if none match and all are set + message: message.join(', ') + }; inputResultsArr.push(resultObj); return resultObj; } - } else { - if (!Array.isArray(nestedResultArr)) nestedResultArr = []; - let propertyArr = property.split('.'); - propertyArr.shift(); - property = propertyArr.join('.'); - let localConditionResult = validate(condition, conditionResult, inputResultsArr, message, condition.property, condition.parsed); - - let resultObj = { - status: localConditionResult, - message: message.join(', ') - }; + } + if (condition.transform && condition.transform == 'EACH' && condition) { + if (condition.op == 'CONTAINS') { + var stringifiedCondition = JSON.stringify(condition.parsed); + if (condition.value && condition.value.includes(':')) { + var key = condition.value.split(/:(?!.*:)/)[0]; + var value = condition.value.split(/:(?!.*:)/)[1]; + if (stringifiedCondition.includes(key) && stringifiedCondition.includes(value)){ + message.push(`${property}: ${condition.value} found in ${stringifiedCondition}`); + conditionResult = 0; + } else { + message.push(`${condition.value} not found in ${stringifiedCondition}`); + conditionResult = 2; + } + } else if (stringifiedCondition && stringifiedCondition.includes(condition.value)) { + message.push(`${property}: ${condition.value} found in ${stringifiedCondition}`); + conditionResult = 0; + } else if (stringifiedCondition && stringifiedCondition.length){ + message.push(`${condition.value} not found in ${stringifiedCondition}`); + conditionResult = 2; + } else { + message.push(`${condition.parsed} is not the right property type for this operation`); + conditionResult = 2; + } + } else if (condition.op == 'NOTCONTAINS') { + var conditionStringified = JSON.stringify(condition.parsed); + if (condition.value && condition.value.includes(':')) { - inputResultsArr.push(resultObj); - return resultObj; + var conditionKey = condition.value.split(/:(?!.*:)/)[0]; + var conditionValue = condition.value.split(/:(?!.*:)/)[1]; + if (conditionStringified.includes(conditionKey) && !conditionStringified.includes(conditionValue)){ + message.push(`${property}: ${condition.value} not found in ${conditionStringified}`); + return 0; + } else { + message.push(`${condition.value} found in ${conditionStringified}`); + return 2; + } + } else if (conditionStringified && !conditionStringified.includes(condition.value)) { + message.push(`${property}: ${condition.value} not found in ${conditionStringified}`); + return 0; + } else if (conditionStringified && conditionStringified.length){ + message.push(`${condition.value} found in ${conditionStringified}`); + return 2; + } else { + message.push(`${condition.parsed} is not the right property type for this operation`); + return 2; + } + } else { + // Recurse into the same function + var subProcessed = []; + if (!condition.parsed.length) { + conditionResult = 2; + message.push(`${property}: is not iterable using EACH transformation`); + } else { + condition.parsed.forEach(function(parsed) { + subProcessed.push(runValidation(parsed, condition, inputResultsArr, null, region, cloud, accountId, resourceId)); + }); + subProcessed.forEach(function(sub) { + if (sub.status) conditionResult = sub.status; + if (sub.message) message.push(sub.message); + }); + } + } + } else if (condition.op == 'EQ') { + if (condition.parsed == condition.value) { + message.push(`${property}: ${condition.parsed} matched: ${condition.value}`); + conditionResult = 0; + } else { + // Check if we're comparing an object to a string - common user error + if (typeof condition.parsed === 'object' && condition.parsed !== null && typeof condition.value === 'string') { + message.push(`${property}: is an object but compared to string "${condition.value}". Consider using a more specific property path like "${property}.propertyName"`); + } else { + message.push(`${property}: ${condition.parsed} did not match: ${condition.value}`); + } + conditionResult = 2; + } + } else if (condition.op == 'GT') { + // Convert to numbers for comparison if they are numeric strings + let parsedVal = condition.parsed; + let comparisonVal = condition.value; + + // Force numeric conversion + parsedVal = Number(parsedVal); + comparisonVal = Number(comparisonVal); + + if (parsedVal > comparisonVal) { + message.push(`${property}: count of ${condition.parsed} was greater than: ${condition.value}`); + conditionResult = 0; + } else { + conditionResult = 2; + message.push(`${property}: count of ${condition.parsed} was not greater than: ${condition.value}`); + } + } else if (condition.op == 'LT') { + // Convert to numbers for comparison if they are numeric strings + let parsedVal = condition.parsed; + let comparisonVal = condition.value; + + // Force numeric conversion + parsedVal = Number(parsedVal); + comparisonVal = Number(comparisonVal); + + if (parsedVal < comparisonVal) { + message.push(`${property}: count of ${condition.parsed} was less than: ${condition.value}`); + conditionResult = 0; + } else { + conditionResult = 2; + message.push(`${property}: count of ${condition.parsed} was not less than: ${condition.value}`); + } + } else if (condition.op == 'NE') { + if (condition.parsed !== condition.value) { + message.push(`${property}: ${condition.parsed} is not: ${condition.value}`); + conditionResult = 0; + } else { + conditionResult = 2; + // Check if we're comparing an object to a string - common user error + if (typeof condition.parsed === 'object' && condition.parsed !== null && typeof condition.value === 'string') { + message.push(`${property}: is an object but compared to string "${condition.value}". Consider using a more specific property path like "${property}.propertyName"`); + } else { + message.push(`${property}: ${condition.parsed} is: ${condition.value}`); + } + } + } else if (condition.op == 'MATCHES') { + if (userRegex.test(condition.parsed)) { + message.push(`${property}: ${condition.parsed} matches the regex: ${condition.value}`); + conditionResult = 0; + } else { + conditionResult = 2; + message.push(`${property}: ${condition.parsed} does not match the regex: ${condition.value}`); + } + } else if (condition.op == 'NOTMATCHES') { + if (!userRegex.test(condition.parsed)) { + message.push(`${condition.property}: ${condition.parsed} does not match the regex: ${condition.value}`); + conditionResult = 0; + } else { + conditionResult = 2; + message.push(`${condition.property}: ${condition.parsed} matches the regex : ${condition.value}`); + } + } else if (condition.op == 'EXISTS') { + if (condition.parsed !== 'not set') { + message.push(`${property}: set to ${condition.parsed}`); + conditionResult = 0; + } else { + message.push(`${property}: ${condition.parsed}`); + conditionResult = 2; + } + } else if (condition.op == 'ISTRUE') { + if (typeof condition.parsed == 'boolean' && condition.parsed) { + message.push(`${property} is true`); + conditionResult = 0; + } else if (typeof condition.parsed == 'boolean' && !condition.parsed) { + conditionResult = 2; + message.push(`${property} is false`); + } else { + message.push(`${property} is not a boolean value`); + conditionResult = 2; + } + } else if (condition.op == 'ISFALSE') { + if (typeof condition.parsed == 'boolean' && !condition.parsed) { + message.push(`${property} is false`); + conditionResult = 0; + } else if (typeof condition.parsed == 'boolean' && condition.parsed) { + conditionResult = 2; + message.push(`${property} is true`); + } else { + message.push(`${property} is not a boolean value`); + conditionResult = 2; + } + } else if (condition.op == 'ISEMPTY') { + if (condition.parsed === 'not set') { + message.push(`${property} is not set`); + conditionResult = 2; + } else if (typeof condition.parsed === 'boolean' || typeof condition.parsed === 'number') { + message.push(`${property} is of type ${typeof condition.parsed}, which cannot be empty`); + conditionResult = 2; + } else if (condition.parsed === '' || + (Array.isArray(condition.parsed) && condition.parsed.length === 0) || + (typeof condition.parsed === 'object' && condition.parsed !== null && Object.keys(condition.parsed).length === 0)) { + message.push(`${property} is empty`); + conditionResult = 0; + } else { + message.push(`${property} is not empty`); + conditionResult = 2; + } + } else if (condition.op == 'CONTAINS' && condition.transform == 'IPRANGE') { + if (typeof condition.parsed !== 'string') { + message.push(property + ': IPRANGE requires property to be an IP address string, got ' + typeof condition.parsed); + conditionResult = 2; + } else { + var valueRange = transformToIpRange(condition.value); + if (valueRange.error) { + message.push(property + ': ' + valueRange.error); + conditionResult = 2; + } else { + var cidrResult = inCidr(condition.value, condition.parsed); + + if (cidrResult.error) { + message.push(property + ': ' + cidrResult.error); + conditionResult = 2; + } else if (cidrResult.result) { + message.push(property + ': ' + cidrResult.message + ' (' + condition.parsed + ' allows access from ' + condition.value + ')'); + conditionResult = 0; + } else { + message.push(property + ': ' + cidrResult.message + ' (' + condition.parsed + ' does not allow access from ' + condition.value + ')'); + conditionResult = 2; + } + } + } + } else if (condition.op == 'CONTAINS') { + if (condition.parsed && condition.parsed.length && condition.parsed.includes(condition.value)) { + message.push(`${property}: ${condition.value} found in ${condition.parsed}`); + conditionResult = 0; + } else if (condition.parsed && condition.parsed.length){ + message.push(`${condition.value} not found in ${condition.parsed}`); + conditionResult = 2; + } else { + // Check if we're trying to use CONTAINS on an object - common user error + if (typeof condition.parsed === 'object' && condition.parsed !== null && !Array.isArray(condition.parsed)) { + message.push(`${property}: is an object, not a string or array. CONTAINS operation requires a string or array. Consider using a more specific property path like "${property}.propertyName"`); + } else { + message.push(`${condition.parsed} is not the right property type for this operation`); + } + conditionResult = 2; + } + } else if (condition.op == 'NOTCONTAINS') { + if (condition.parsed && condition.parsed.length && !condition.parsed.includes(condition.value)) { + message.push(`${property}: ${condition.value} not found in ${condition.parsed}`); + conditionResult = 0; + } else if (condition.parsed && condition.parsed.length){ + message.push(`${condition.value} found in ${condition.parsed}`); + conditionResult = 2; + } else { + message.push(`${condition.parsed} is not the right property type for this operation`); + conditionResult = 2; + } } - } else { - // Transform the property if required - conditionResult = validate(condition, conditionResult, inputResultsArr, message, property); - if (conditionResult) result = conditionResult; } } @@ -436,7 +785,7 @@ var runValidation = function(obj, condition, inputResultsArr, nestedResultArr) { } let resultObj = { - status: result, + status: conditionResult, message: message.join(', ') }; @@ -445,92 +794,281 @@ var runValidation = function(obj, condition, inputResultsArr, nestedResultArr) { }; var runConditions = function(input, data, results, resourcePath, resourceName, region, cloud, accountId) { - let dataToValidate; - let newPath; - let newData; - let validated; let parsedResource = resourceName; - let inputResultsArr = []; let logical; let localInput = JSON.parse(JSON.stringify(input)); // to check if top level * matches. ex: Instances[*] should be // present in each condition if not its impossible to compare resources - let resourceConditionArr = []; + localInput.conditions.forEach(condition => { logical = condition.logical; - var conditionPropArr = condition.property.split('.'); - if (condition.property && condition.property.includes('[*]')) { - if (conditionPropArr.length > 1 && conditionPropArr[1].includes('[*]')) { - resourceConditionArr.push(conditionPropArr[0]); - var firstProperty = conditionPropArr.shift(); - dataToValidate = parse(data, firstProperty.split('[*]')[0])[0]; - condition.property = conditionPropArr.join('.'); - if (dataToValidate.length) { - dataToValidate.forEach(newData => { - condition.validated = runValidation(newData, condition, inputResultsArr); - parsedResource = parse(newData, resourcePath, region, cloud, accountId, resourceName)[0]; - if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName; - }); - } else { - condition.validated = runValidation([], condition, inputResultsArr); - parsedResource = parse([], resourcePath, region, cloud, accountId, resourceName)[0]; - if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName; - } - // result per resource - } else { - dataToValidate = parse(data, condition.property); - newPath = dataToValidate[1]; - newData = dataToValidate[0]; - if (newPath && newData.length){ - newData.forEach(dataElm =>{ - if (newPath) condition.property = JSON.parse(JSON.stringify(newPath)); - condition.validated = runValidation(dataElm, condition, inputResultsArr); - parsedResource = parse(dataElm, resourcePath, region, cloud, accountId, resourceName)[0]; - if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName; - }); - } else if (newPath && !newData.length) { - condition.property = JSON.parse(JSON.stringify(newPath)); - condition.validated = runValidation(newData, condition, inputResultsArr); - parsedResource = parse(newData, resourcePath, region, cloud, accountId, resourceName)[0]; - if (parsedResource === 'not set' || typeof parsedResource !== 'string') parsedResource = resourceName; - } else if (!newPath) { - // no path returned. means it has fully parsed and got the value. - // save the value - newPath = JSON.parse(JSON.stringify(condition.property)); - if (condition.property.includes('.')){ - condition.property = condition.property.split('.')[condition.property.split('.').length -1 ]; + + // Special handling for ResourceRecordSets[*].AliasTarget.* properties + if (condition.property && condition.property.includes('ResourceRecordSets[*].AliasTarget')) { + let foundMatch = false; + let matchResults = []; + let nonMatchResults = []; + + if (data && data.ResourceRecordSets && Array.isArray(data.ResourceRecordSets)) { + // Directly access ResourceRecordSets if it exists at the top level + for (let i = 0; i < data.ResourceRecordSets.length; i++) { + let record = data.ResourceRecordSets[i]; + if (record && record.AliasTarget) { + // Extract just the AliasTarget part of the property path + const aliasProperty = condition.property.split('AliasTarget.')[1]; + + if (aliasProperty && record.AliasTarget[aliasProperty]) { + let propValue = record.AliasTarget[aliasProperty]; + let result = 2; // Default to fail + let message = ''; + + // Perform the actual comparison + if (condition.op === 'CONTAINS' && propValue.includes(condition.value)) { + result = 0; + message = `${aliasProperty}: ${condition.value} found in ${propValue}`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else if (condition.op === 'NOTCONTAINS' && !propValue.includes(condition.value)) { + result = 0; + message = `${aliasProperty}: ${condition.value} not found in ${propValue}`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else if (condition.op === 'EQ' && propValue === condition.value) { + result = 0; + message = `${aliasProperty}: ${propValue} matched: ${condition.value}`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else if (condition.op === 'NE' && propValue !== condition.value) { + result = 0; + message = `${aliasProperty}: ${propValue} is not: ${condition.value}`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else if (condition.op === 'GT') { + // Convert to numbers for comparison if they are numeric strings + let parsedVal = Number(propValue); + let comparisonVal = Number(condition.value); + + if (!isNaN(parsedVal) && !isNaN(comparisonVal) && parsedVal > comparisonVal) { + result = 0; + message = `${aliasProperty}: ${propValue} was greater than: ${condition.value}`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else { + message = `${aliasProperty}: ${propValue} was not greater than: ${condition.value}`; + nonMatchResults.push({ + status: 2, + message: message, + resource: record.Name || resourceName + }); + } + } else if (condition.op === 'LT') { + // Convert to numbers for comparison if they are numeric strings + let parsedVal = Number(propValue); + let comparisonVal = Number(condition.value); + + if (!isNaN(parsedVal) && !isNaN(comparisonVal) && parsedVal < comparisonVal) { + result = 0; + message = `${aliasProperty}: ${propValue} was less than: ${condition.value}`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else { + message = `${aliasProperty}: ${propValue} was not less than: ${condition.value}`; + nonMatchResults.push({ + status: 2, + message: message, + resource: record.Name || resourceName + }); + } + } else if (condition.op === 'ISTRUE') { + if (typeof propValue === 'boolean' && propValue === true) { + result = 0; + message = `${aliasProperty} is true`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else if (typeof propValue === 'string' && + (propValue.toLowerCase() === 'true' || propValue === '1')) { + result = 0; + message = `${aliasProperty} is true (${propValue})`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else { + message = `${aliasProperty} is not true`; + nonMatchResults.push({ + status: 2, + message: message, + resource: record.Name || resourceName + }); + } + } else if (condition.op === 'ISFALSE') { + if (typeof propValue === 'boolean' && propValue === false) { + result = 0; + message = `${aliasProperty} is false`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else if (typeof propValue === 'string' && + (propValue.toLowerCase() === 'false' || propValue === '0')) { + result = 0; + message = `${aliasProperty} is false (${propValue})`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else { + message = `${aliasProperty} is not false`; + nonMatchResults.push({ + status: 2, + message: message, + resource: record.Name || resourceName + }); + } + } else if (condition.op === 'EXISTS') { + result = 0; + message = `${aliasProperty}: set to ${propValue}`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else if (condition.op === 'MATCHES' && new RegExp(condition.value).test(propValue)) { + result = 0; + message = `${aliasProperty}: ${propValue} matches the regex: ${condition.value}`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else if (condition.op === 'NOTMATCHES' && !new RegExp(condition.value).test(propValue)) { + result = 0; + message = `${aliasProperty}: ${propValue} does not match the regex: ${condition.value}`; + foundMatch = true; + matchResults.push({ + status: result, + message: message, + resource: record.Name || resourceName + }); + } else { + if (condition.op === 'CONTAINS') { + message = `${condition.value} not found in ${propValue}`; + } else if (condition.op === 'NOTCONTAINS') { + message = `${condition.value} found in ${propValue}`; + } else if (condition.op === 'EQ') { + message = `${aliasProperty}: ${propValue} did not match: ${condition.value}`; + } else if (condition.op === 'NE') { + message = `${aliasProperty}: ${propValue} is: ${condition.value}`; + } else if (condition.op === 'GT') { + message = `${aliasProperty}: ${propValue} was not greater than: ${condition.value}`; + } else if (condition.op === 'LT') { + message = `${aliasProperty}: ${propValue} was not less than: ${condition.value}`; + } else if (condition.op === 'ISTRUE') { + message = `${aliasProperty} is not true`; + } else if (condition.op === 'ISFALSE') { + message = `${aliasProperty} is not false`; + } else if (condition.op === 'MATCHES') { + message = `${aliasProperty}: ${propValue} does not match the regex: ${condition.value}`; + } else if (condition.op === 'NOTMATCHES') { + message = `${aliasProperty}: ${propValue} matches the regex: ${condition.value}`; + } + + nonMatchResults.push({ + status: 2, + message: message, + resource: record.Name || resourceName + }); + } + } else if (!aliasProperty) { + // Handle the entire AliasTarget object + matchResults.push({ + status: 0, + message: `AliasTarget: exists for record ${record.Name}`, + resource: record.Name || resourceName + }); + foundMatch = true; + } } - condition.validated = runValidation(newData, condition, inputResultsArr); - condition.property = JSON.parse(JSON.stringify(newPath)); - parsedResource = parse(newData, resourcePath, region, cloud, accountId, resourceName)[0]; - if (parsedResource === 'not set' || typeof parsedResource !== 'string') parsedResource = resourceName; } } - } else { - dataToValidate = parse(data, condition.property); - if (dataToValidate.length === 1) { - validated = runValidation(data, condition, inputResultsArr); - parsedResource = parse(data, resourcePath, region, cloud, accountId, resourceName)[0]; - if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName; - } else { - newPath = dataToValidate[1]; - newData = dataToValidate[0]; - condition.property = newPath; - newData.forEach(element =>{ - condition.validated = runValidation(element, condition, inputResultsArr); - parsedResource = parse(data, resourcePath, region, cloud, accountId, resourceName)[0]; - if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = null; - - results.push({ - status: validated.status, - resource: parsedResource ? parsedResource : resourceName, - message: validated.message, - region: region + + // After checking all records, add the appropriate results to inputResultsArr + if (foundMatch) { + // If any record matched, add all matching results + matchResults.forEach(result => { + inputResultsArr.push({ + status: result.status, + message: result.message }); + parsedResource = result.resource; }); + } else { + // If no records matched, add a failure result + if (nonMatchResults.length > 0) { + // Use the first non-matching result as the representative failure + inputResultsArr.push({ + status: 2, + message: nonMatchResults[0].message + }); + parsedResource = nonMatchResults[0].resource; + } else { + // No records with AliasTarget found + inputResultsArr.push({ + status: 2, + message: `No matching records with AliasTarget.${condition.property.split('AliasTarget.')[1] || ''} found` + }); + } } + } else if (condition.property && condition.property.includes('[*]')) { + // For wildcard properties, parse once and validate the result + const parseResult = parse(data, condition.property, region, cloud, accountId, resourceName); + condition.parsed = parseResult; + condition.validated = runValidation(data, condition, inputResultsArr, null, region, cloud, accountId, resourceName); + parsedResource = parse(data, resourcePath, region, cloud, accountId, resourceName); + if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName; + } else { + // For non-wildcard properties, use the same logic as wildcard + condition.validated = runValidation(data, condition, inputResultsArr, null, region, cloud, accountId, resourceName); + parsedResource = parse(data, resourcePath, region, cloud, accountId, resourceName); + if (typeof parsedResource !== 'string' || parsedResource === 'not set') parsedResource = resourceName; } }); @@ -568,13 +1106,13 @@ var asl = function(source, input, resourceMap, cloud, accountId, callback) { message: regionVal.err.message || 'Error', region: region }); - } else if (regionVal.data && regionVal.data.length) { + } else if (regionVal.data && regionVal.data.length) { regionVal.data.forEach(function(regionData) { var resourceName = parse(regionData, resourcePath, region, cloud, accountId)[0]; runConditions(input, regionData, results, resourcePath, resourceName, region, cloud, accountId); }); } else if (regionVal.data && Object.keys(regionVal.data).length) { - runConditions(input, regionVal.data, results, resourcePath, '', region); + runConditions(input, regionVal.data, results, resourcePath, '', region, cloud, accountId); } else { if (!Object.keys(regionVal).length || (regionVal.data && (!regionVal.data.length || !Object.keys(regionVal.data).length))) { results.push({ @@ -599,7 +1137,6 @@ var asl = function(source, input, resourceMap, cloud, accountId, callback) { runConditions(input, regionData, results, resourcePath, resourceName, region, cloud, accountId); }); } else { - runConditions(input, resourceObj.data, results, resourcePath, resourceName, region, cloud, accountId); } } @@ -611,4 +1148,4 @@ var asl = function(source, input, resourceMap, cloud, accountId, callback) { callback(null, results, data); }; -module.exports = asl; +module.exports = asl; \ No newline at end of file diff --git a/helpers/aws/api.js b/helpers/aws/api.js index 3cc10d910..1f328c909 100644 --- a/helpers/aws/api.js +++ b/helpers/aws/api.js @@ -144,15 +144,6 @@ var serviceMap = { BridgeResourceNameIdentifier: 'DomainName', BridgeExecutionService: 'ES', BridgeCollectionService: 'es', DataIdentifier: 'DomainStatus', }, - 'QLDB': - { - enabled: true, isSingleSource: true, InvAsset: 'ledger', InvService: 'qldb', - InvResourceCategory: 'database', InvResourceType: 'qldb_ledger', BridgeServiceName: 'qldb', - BridgePluginCategoryName: 'QLDB', BridgeProvider: 'aws', BridgeCall: 'describeLedger', - BridgeArnIdentifier: 'Arn', BridgeIdTemplate: '', BridgeResourceType: 'ledger', - BridgeResourceNameIdentifier: 'Name', BridgeExecutionService: 'QLDB', - BridgeCollectionService: 'qldb', DataIdentifier: 'data', - }, 'DynamoDB': { enabled: true, isSingleSource: true, InvAsset: 'table', InvService: 'dynamodb', @@ -216,6 +207,7 @@ var serviceMap = { BridgeResourceNameIdentifier: 'logGroupName', BridgeExecutionService: 'CloudWatchLogs', BridgeCollectionService: 'cloudwatchlogs', DataIdentifier: 'data', }, + 'EventBridge': { enabled: true, isSingleSource: true, InvAsset: 'bus', InvService: 'eventbridge', @@ -225,6 +217,15 @@ var serviceMap = { BridgeResourceNameIdentifier: 'Name', BridgeExecutionService: 'EventBridge', BridgeCollectionService: 'eventbridge', DataIdentifier: 'data', }, + 'ECR': + { + enabled: true, isSingleSource: true, InvAsset: 'registry', InvService: 'ecr', + InvResourceCategory: 'cloud_resources', InvResourceType: 'ecr_repository', + BridgeServiceName: 'ecr', BridgePluginCategoryName: 'ECR', BridgeProvider: 'aws', BridgeCall: 'describeRepositories', + BridgeArnIdentifier: 'repositoryArn', BridgeIdTemplate: '', BridgeResourceType: 'repository', + BridgeResourceNameIdentifier:'repositoryName' , BridgeExecutionService: 'ECR', + BridgeCollectionService: 'ecr', DataIdentifier: 'data', + }, 'App Mesh': { enabled: true, isSingleSource: true, InvAsset: 'mesh', InvService: 'appmesh', @@ -461,15 +462,6 @@ var serviceMap = { BridgeResourceNameIdentifier: 'EnvironmentName', BridgeExecutionService: 'ElasticBeanstalk', BridgeCollectionService: 'elasticbeanstalk', BridgeCall: 'describeEnvironments', DataIdentifier: 'data', }, - 'Elastic Transcoder': - { - enabled: true, isSingleSource: true, InvAsset: 'transcoder', InvService: 'elasticTranscoder', - InvResourceCategory: 'cloud_resources', InvResourceType: 'transcoder pipeline', - BridgeProvider: 'aws', BridgeServiceName: 'elastictranscoder', BridgePluginCategoryName: 'Elastic Transcoder', - BridgeArnIdentifier: 'Arn', BridgeIdTemplate: '', BridgeResourceType: 'pipeline', - BridgeResourceNameIdentifier: 'Name', BridgeExecutionService: 'Elastic Transcoder', - BridgeCollectionService: 'elastictranscoder', BridgeCall: 'listPipelines', DataIdentifier: 'data', - }, 'ELBv2': { enabled: true, isSingleSource: true, InvAsset: 'loadbalancer', InvService: 'elbv2', @@ -557,7 +549,7 @@ var serviceMap = { InvResourceCategory: 'ai&ml', InvResourceType: 'Lookout Metrics', BridgeProvider: 'aws', BridgeServiceName: 'lookoutmetrics', BridgePluginCategoryName: 'AI & ML', BridgeArnIdentifier: 'AnomalyDetectorArn', BridgeIdTemplate: '', - BridgeResourceType: 'lookoutmetrics', BridgeResourceNameIdentifier: 'AnomalyDetectorName', BridgeExecutionService: 'AI & ML', + BridgeResourceType: 'AnomalyDetector', BridgeResourceNameIdentifier: 'AnomalyDetectorName', BridgeExecutionService: 'AI & ML', BridgeCollectionService: 'lookoutmetrics', BridgeCall: 'listAnomalyDetectors', DataIdentifier: 'data', }, { @@ -569,6 +561,123 @@ var serviceMap = { BridgeCollectionService: 'sagemaker', BridgeCall: 'describeNotebookInstance', DataIdentifier: 'data', }, ], + 'Guard Duty': + { + enabled: true, isSingleSource: true, InvAsset: 'detector', InvService: 'guardduty', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Guardduty Detector', + BridgeProvider: 'aws', BridgeServiceName: 'guardduty', BridgePluginCategoryName: 'GuardDuty', + BridgeArnIdentifier: '', BridgeIdTemplate: 'arn:aws:guardduty:{region}:{cloudAccount}:detector/{id}', BridgeResourceType: 'detector', + BridgeResourceNameIdentifier: 'id', BridgeExecutionService: 'GuardDuty', + BridgeCollectionService: 'guardduty', BridgeCall: 'getDetector', DataIdentifier: 'data', + }, + 'WorkSpaces': + { + enabled: true, isSingleSource: true, InvAsset: 'instance', InvService: 'workspaces', + InvResourceCategory: 'cloud_resources', InvResourceType: 'WorkSpace Instance', + BridgeProvider: 'aws', BridgeServiceName: 'workspaces', BridgePluginCategoryName: 'WorkSpaces', + BridgeArnIdentifier: '', BridgeIdTemplate: 'arn:aws:workspaces:{region}:{cloudAccount}:workspace/{name}', BridgeResourceType: 'workspace', + BridgeResourceNameIdentifier: 'WorkspaceId', BridgeExecutionService: 'WorkSpaces', + BridgeCollectionService: 'workspaces', BridgeCall: 'describeWorkspaces', DataIdentifier: 'data', + }, + 'Transfer': + { + enabled: true, isSingleSource: true, InvAsset: 'server', InvService: 'transfer', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Transfer Server', + BridgeProvider: 'aws', BridgeServiceName: 'transfer', BridgePluginCategoryName: 'Transfer', + BridgeArnIdentifier: 'Arn', BridgeIdTemplate: '', BridgeResourceType: 'server', + BridgeResourceNameIdentifier: 'ServerId', BridgeExecutionService: 'Transfer', + BridgeCollectionService: 'transfer', BridgeCall: 'listServers', DataIdentifier: 'data', + }, + 'AppFlow': + { + enabled: true, isSingleSource: true, InvAsset: 'flow', InvService: 'appflow', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Appflow', + BridgeProvider: 'aws', BridgeServiceName: 'appflow', BridgePluginCategoryName: 'AppFlow', + BridgeArnIdentifier: 'flowArn', BridgeIdTemplate: '', BridgeResourceType: 'flow', + BridgeResourceNameIdentifier: 'flowName', BridgeExecutionService: 'AppFlow', + BridgeCollectionService: 'appflow', BridgeCall: 'listFlows', DataIdentifier: 'data', + }, + 'Cognito': + { + enabled: true, isSingleSource: true, InvAsset: 'userpool', InvService: 'cognitoidentityserviceprovider', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Cognito Userpool', + BridgeProvider: 'aws', BridgeServiceName: 'cognitoidentityserviceprovider', BridgePluginCategoryName: 'Cognito', + BridgeArnIdentifier: '', BridgeIdTemplate: 'arn:aws:cognito-idp:{region}:{cloudAccount}:userpool/{id}', BridgeResourceType: 'userpool', + BridgeResourceNameIdentifier: 'Id', BridgeExecutionService: 'Cognito', + BridgeCollectionService: 'cognitoidentityserviceprovider', BridgeCall: 'listUserPools', DataIdentifier: 'data', + }, + 'WAF': + { + enabled: true, isSingleSource: true, InvAsset: 'webacl', InvService: 'wafv2', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Web ACL', + BridgeProvider: 'aws', BridgeServiceName: 'wafv2', BridgePluginCategoryName: 'WAF', + BridgeArnIdentifier: 'ARN', BridgeIdTemplate: '', BridgeResourceType: 'webacl', + BridgeResourceNameIdentifier: 'Id', BridgeExecutionService: 'WAF', + BridgeCollectionService: 'wafv2', BridgeCall: 'listWebACLs', DataIdentifier: 'data', + }, + 'Glue': + { + enabled: true, isSingleSource: true, InvAsset: 'glue', InvService: 'glue', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Glue SecurityConfigurations', + BridgeProvider: 'aws', BridgeServiceName: 'glue', BridgePluginCategoryName: 'Glue', + BridgeArnIdentifier: '', BridgeIdTemplate: 'arn:aws:glue:{region}:{cloudAccount}:/securityConfiguration/{name}', BridgeResourceType: 'securityConfiguration', + BridgeResourceNameIdentifier: 'Name', BridgeExecutionService: 'Glue', + BridgeCollectionService: 'glue', BridgeCall: 'getSecurityConfigurations', DataIdentifier: 'data', + }, + 'ConfigService': + { + enabled: true, isSingleSource: true, InvAsset: 'configservice', InvService: 'configservice', + InvResourceCategory: 'cloud_resources', InvResourceType: 'ConfigService', + BridgeProvider: 'aws', BridgeServiceName: 'configservice', BridgePluginCategoryName: 'ConfigService', + BridgeArnIdentifier: 'ConfigRuleArn', BridgeIdTemplate: '', BridgeResourceType: 'config-rule', + BridgeResourceNameIdentifier: 'ConfigRuleName', BridgeExecutionService: 'ConfigService', + BridgeCollectionService: 'configservice', BridgeCall: 'describeConfigRules', DataIdentifier: 'data', + }, + 'Firehose': + { + enabled: true, isSingleSource: true, InvAsset: 'firehose', InvService: 'firehose', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Firehose', + BridgeProvider: 'aws', BridgeServiceName: 'firehose', BridgePluginCategoryName: 'Firehose', + BridgeArnIdentifier: 'DeliveryStreamARN', BridgeIdTemplate: '', BridgeResourceType: 'deliverystream', + BridgeResourceNameIdentifier: 'DeliveryStreamName', BridgeExecutionService: 'Firehose', + BridgeCollectionService: 'firehose', BridgeCall: 'describeDeliveryStream', DataIdentifier: 'DeliveryStreamDescription', + }, + 'SES': + { + enabled: true, isSingleSource: true, InvAsset: 'ses', InvService: 'SES', + InvResourceCategory: 'cloud_resource', InvResourceType: 'ses_emails', + BridgeProvider: 'aws', BridgeServiceName: 'ses', BridgePluginCategoryName: 'SES', + BridgeArnIdentifier: '', BridgeIdTemplate: 'arn:aws:ses:{region}:{cloudAccount}:identity/{name}', BridgeResourceType: 'identity', + BridgeResourceNameIdentifier: 'identityName', BridgeExecutionService: 'SES', + BridgeCollectionService: 'ses', BridgeCall: 'getIdentityDkimAttributes', DataIdentifier: 'DkimAttributes', + }, + 'FSx': + { + enabled: true, isSingleSource: true, InvAsset: 'filesystem', InvService: 'fsx', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Fsx Filesystem', + BridgeProvider: 'aws', BridgeServiceName: 'fsx', BridgePluginCategoryName: 'FSx', + BridgeArnIdentifier: 'ResourceARN', BridgeIdTemplate: '', BridgeResourceType: 'file-system', + BridgeResourceNameIdentifier: 'FileSystemId', BridgeExecutionService: 'FSx', + BridgeCollectionService: 'fsx', BridgeCall: 'describeFileSystems', DataIdentifier: 'data', + }, + 'OpenSearch': [ + { + enabled: true, isSingleSource: true, InvAsset: 'domain', InvService: 'opensearch', + InvResourceCategory: 'database', InvResourceType: 'OpenSearch Domain', + BridgeProvider: 'aws', BridgeServiceName: 'opensearch', BridgePluginCategoryName: 'OpenSearch', + BridgeArnIdentifier: 'ARN', BridgeIdTemplate: '', BridgeResourceType: 'domain', + BridgeResourceNameIdentifier: 'DomainName', BridgeExecutionService: 'OpenSearch', + BridgeCollectionService: 'opensearch', BridgeCall: 'describeDomain', DataIdentifier: 'DomainStatus', + }, + { + enabled: true, isSingleSource: true, InvAsset: 'collection', InvService: 'opensearch', + InvResourceCategory: 'database', InvResourceType: 'OpenSearch Serverless', + BridgeProvider: 'aws', BridgeServiceName: 'opensearchserverless', BridgePluginCategoryName: 'OpenSearch', + BridgeArnIdentifier: 'arn', BridgeIdTemplate: '', BridgeResourceType: 'collection', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'OpenSearch', + BridgeCollectionService: 'opensearchserverless', BridgeCall: 'listCollections', DataIdentifier: 'data', + }, + ], }; var calls = { @@ -1146,14 +1255,6 @@ var calls = { paginate: 'NextToken' } }, - ElasticTranscoder: { - // TODO: Pagination via NextPageToken and PageToken - listPipelines: { - property: 'Pipelines', - paginate: 'NextPageToken', - paginateReqProp: 'PageToken' - } - }, ELB: { describeLoadBalancers: { property: 'LoadBalancerDescriptions', @@ -1479,12 +1580,6 @@ var calls = { paginate: 'nextToken' } }, - QLDB: { - listLedgers: { - property: 'Ledgers', - paginate: 'NextToken' - } - }, RDS: { describeDBInstances: { property: 'DBInstances', @@ -1818,6 +1913,21 @@ var postcalls = [ IoTSiteWise: { sendIntegration: serviceMap['IoT SiteWise'] }, + Workspaces: { + sendIntegration: serviceMap['WorkSpaces'] + }, + Transfer: { + sendIntegration: serviceMap['Transfer'] + }, + Glue: { + sendIntegration: serviceMap['Glue'], + }, + SecurityHub: { + sendIntegration: serviceMap['SecurityHub'] + }, + FSx:{ + sendIntegration: serviceMap['FSx'] + }, ACM: { describeCertificate: { @@ -1825,7 +1935,10 @@ var postcalls = [ reliesOnCall: 'listCertificates', filterKey: 'CertificateArn', filterValue: 'CertificateArn' - } + }, + sendIntegration: { + enabled: true + }, }, AccessAnalyzer: { listFindings: { @@ -1913,7 +2026,8 @@ var postcalls = [ reliesOnCall: 'listFlows', filterKey: 'flowName', filterValue: 'flowName' - } + }, + sendIntegration: serviceMap['AppFlow'] }, Athena: { getWorkGroup: { @@ -2102,7 +2216,8 @@ var postcalls = [ reliesOnCall: 'describeConfigRules', filterKey: 'ConfigRuleName', filterValue: 'ConfigRuleName' - } + }, + sendIntegration: serviceMap['ConfigService'] }, CodeStar: { describeProject: { @@ -2227,7 +2342,8 @@ var postcalls = [ reliesOnCall: 'listDomainNames', filterKey: 'DomainName', filterValue: 'DomainName' - } + }, + sendIntegration: serviceMap['OpenSearch'][0] }, S3: { getBucketLogging: { @@ -2342,7 +2458,8 @@ var postcalls = [ reliesOnCall: 'listUserPools', filterKey: 'UserPoolId', filterValue: 'Id' - } + }, + sendIntegration: serviceMap['Cognito'] }, EC2: { describeSubnets: { @@ -2388,9 +2505,7 @@ var postcalls = [ filterKey: 'resourceArn', filterValue: 'repositoryArn' }, - sendIntegration: { - enabled: true - } + sendIntegration: serviceMap['ECR'] }, ECRPUBLIC: { describeRepositories: { @@ -2437,15 +2552,6 @@ var postcalls = [ }, sendIntegration: serviceMap['ElasticBeanstalk'] }, - ElasticTranscoder: { - listJobsByPipeline: { - reliesOnService: 'elastictranscoder', - reliesOnCall: 'listPipelines', - filterKey: 'PipelineId', - filterValue: 'Id' - }, - sendIntegration: serviceMap['Elastic Transcoder'] - }, ELB: { describeLoadBalancerPolicies: { reliesOnService: 'elb', @@ -2645,7 +2751,8 @@ var postcalls = [ reliesOnService: 'firehose', reliesOnCall: 'listDeliveryStreams', override: true - } + }, + sendIntegration: serviceMap['Firehose'], }, KMS: { describeKey: { @@ -2684,7 +2791,7 @@ var postcalls = [ reliesOnCall: 'listFunctions', filterKey: 'FunctionName', filterValue: 'FunctionName', - rateLimit: 100, // it's not documented but experimentially 10/second works. + rateLimit: 100, // it's not documented but experimental 10/second works. }, getFunction: { reliesOnService: 'lambda', @@ -2766,15 +2873,6 @@ var postcalls = [ filterValue: 'botAliasId' } }, - QLDB: { - describeLedger: { - reliesOnService: 'qldb', - reliesOnCall: 'listLedgers', - filterKey: 'Name', - filterValue: 'Name' - }, - sendIntegration: serviceMap['QLDB'] - }, ManagedBlockchain: { listMembers: { reliesOnService: 'managedblockchain', @@ -2897,7 +2995,8 @@ var postcalls = [ reliesOnCall: 'listIdentities', override: true, rateLimit: 1000 - } + }, + sendIntegration: serviceMap['SES'] }, SNS: { getTopicAttributes: { @@ -2951,7 +3050,8 @@ var postcalls = [ reliesOnService: 'wafv2', reliesOnCall: 'listWebACLs', override: true - } + }, + sendIntegration: serviceMap['WAF'] }, GuardDuty: { getDetector: { @@ -2974,6 +3074,7 @@ var postcalls = [ reliesOnCall: 'listDetectors', override: true, }, + sendIntegration: serviceMap['Guard Duty'], }, }, { @@ -3130,7 +3231,8 @@ var postcalls = [ reliesOnService: 'opensearchserverless', reliesOnCall: 'listNetworkSecurityPolicies', override: true - } + }, + sendIntegration: serviceMap['OpenSearch'][1] } } ]; @@ -3141,4 +3243,4 @@ module.exports = { calls: calls, postcalls: postcalls, integrationSendLast: integrationSendLast -}; +}; \ No newline at end of file diff --git a/helpers/aws/api_multipart.js b/helpers/aws/api_multipart.js index 4590b8ffb..276538367 100644 --- a/helpers/aws/api_multipart.js +++ b/helpers/aws/api_multipart.js @@ -573,14 +573,6 @@ var calls = [ paginate: 'NextToken' } }, - ElasticTranscoder: { - // TODO: Pagination via NextPageToken and PageToken - listPipelines: { - property: 'Pipelines', - paginate: 'NextPageToken', - paginateReqProp: 'PageToken' - } - }, ELB: { describeLoadBalancers: { property: 'LoadBalancerDescriptions', @@ -862,12 +854,6 @@ var calls = [ paginate: 'nextToken' } }, - QLDB: { - listLedgers: { - property: 'Ledgers', - paginate: 'NextToken' - } - }, RDS: { describeDBInstances: { property: 'DBInstances', @@ -1666,14 +1652,6 @@ var postcalls = [ override: true } }, - ElasticTranscoder: { - listJobsByPipeline: { - reliesOnService: 'elastictranscoder', - reliesOnCall: 'listPipelines', - filterKey: 'PipelineId', - filterValue: 'Id' - } - }, ELB: { describeLoadBalancerPolicies: { reliesOnService: 'elb', @@ -2018,7 +1996,7 @@ var postcalls = [ reliesOnCall: 'listFunctions', filterKey: 'FunctionName', filterValue: 'FunctionName', - rateLimit: 500, // it's not documented but experimentally 10/second works. + rateLimit: 500, // it's not documented but experimental 10/second works. }, getFunction: { reliesOnService: 'lambda', @@ -2092,14 +2070,6 @@ var postcalls = [ filterValue: 'botId' } }, - QLDB: { - describeLedger: { - reliesOnService: 'qldb', - reliesOnCall: 'listLedgers', - filterKey: 'Name', - filterValue: 'Name' - } - }, ManagedBlockchain: { listMembers: { reliesOnService: 'managedblockchain', @@ -2440,4 +2410,4 @@ module.exports = { callsMultipart: calls, postcallsMultipart: postcalls, integrationSendLast: integrationSendLast -}; +}; \ No newline at end of file diff --git a/helpers/azure/api.js b/helpers/azure/api.js index baf16480c..b05c1a76b 100644 --- a/helpers/azure/api.js +++ b/helpers/azure/api.js @@ -53,7 +53,7 @@ var serviceMap = { BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Redis Cache', BridgeCollectionService: 'rediscaches', DataIdentifier: 'data', }, - 'CDN Profiles': + 'CDN Profiles': [ { enabled: true, isSingleSource: true, InvAsset: 'cdnProfiles', InvService: 'cdnProfiles', InvResourceCategory: 'cloud_resources', InvResourceType: 'CDN_Profiles', BridgeServiceName: 'profiles', @@ -62,6 +62,15 @@ var serviceMap = { BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'CDN Profiles', BridgeCollectionService: 'profiles', DataIdentifier: 'data', }, + { + enabled: true, isSingleSource: true, InvAsset: 'endpoint', InvService: 'cdnProfiles', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Endpoints', BridgeServiceName: 'endpoints', + BridgePluginCategoryName: 'CDN Profiles', BridgeProvider: 'Azure', BridgeCall: 'listByProfile', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'endpoints', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'CDN Profiles', + BridgeCollectionService: 'endpoints', DataIdentifier: 'data', + } + ], 'Cosmos DB': { enabled: true, isSingleSource: true, InvAsset: 'cosmosdb', InvService: 'cosmosDB', @@ -116,7 +125,7 @@ var serviceMap = { BridgeResourceNameIdentifier: 'displayName', BridgeExecutionService: 'Azure Policy', BridgeCollectionService: 'policyassignments', DataIdentifier: 'data', }, - 'Virtual Networks': + 'Virtual Networks':[ { enabled: true, isSingleSource: true, InvAsset: 'virtual_network', InvService: 'virtual_network', InvResourceCategory: 'cloud_resources', InvResourceType: 'Virtual Network', BridgeServiceName: 'virtualnetworks', @@ -125,6 +134,15 @@ var serviceMap = { BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Virtual Networks', BridgeCollectionService: 'virtualnetworks', DataIdentifier: 'data', }, + { + enabled: true, isSingleSource: true, InvAsset: 'vn_routeTables', InvService: 'virtual_network', + InvResourceCategory: 'cloud_resources', InvResourceType: 'VN_RouteTables', BridgeServiceName: 'routetables', + BridgePluginCategoryName: 'Virtual Networks', BridgeProvider: 'Azure', BridgeCall: 'listAll', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'routeTables', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Virtual Networks', + BridgeCollectionService: 'routetables', DataIdentifier: 'data', + } + ], 'Queue Service': { enabled: true, isSingleSource: true, InvAsset: 'queueService', InvService: 'queueService', @@ -170,6 +188,96 @@ var serviceMap = { BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'AI & ML', BridgeCollectionService: 'openai', BridgeCall: 'listAccounts', DataIdentifier: 'data', }, + 'Blob Service': + { + enabled: true, isSingleSource: true, InvAsset: 'blob_container', InvService: 'blobservice', + InvResourceCategory: 'cloud_resources', InvResourceType: 'blob_container', BridgeServiceName: 'blobcontainers', + BridgePluginCategoryName: 'Blob Service', BridgeProvider: 'Azure', BridgeCall: 'list', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'containers', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Blob Service', + BridgeCollectionService: 'blobcontainers', DataIdentifier: 'data', + }, + 'Virtual Machines': + { + enabled: true, isSingleSource: true, InvAsset: 'vm_scaleset', InvService: 'virtualmachines', + InvResourceCategory: 'cloud_resources', InvResourceType: 'VM_ScaleSet', BridgeServiceName: 'virtualmachinescalesets', + BridgePluginCategoryName: 'Virtual Machines', BridgeProvider: 'Azure', BridgeCall: 'listAll', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'virtualMachineScaleSets', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Virtual Machines', + BridgeCollectionService: 'virtualmachinescalesets', DataIdentifier: 'data', + }, + 'Event Grid': + { + enabled: true, isSingleSource: true, InvAsset: 'domain', InvService: 'eventgrid', + InvResourceCategory: 'cloud_resources', InvResourceType: 'EventGrid Domain', BridgeServiceName: 'eventgrid', + BridgePluginCategoryName: 'Event Grid', BridgeProvider: 'Azure', BridgeCall: 'listDomains', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'domains', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Event Grid', + BridgeCollectionService: 'eventgrid', DataIdentifier: 'data', + }, + 'Event Hubs': + { + enabled: true, isSingleSource: true, InvAsset: 'namespace', InvService: 'eventhubs', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Eventhubs Namespace', BridgeServiceName: 'eventhub', + BridgePluginCategoryName: 'Event Hubs', BridgeProvider: 'Azure', BridgeCall: 'listEventHub', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'namespaces', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Event Hubs', + BridgeCollectionService: 'eventhub', DataIdentifier: 'data', + }, + 'Defender': [ + { + enabled: true, isSingleSource: true, InvAsset: 'defender', InvService: 'defender', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Defender', BridgeServiceName: 'pricings', + BridgePluginCategoryName: 'Defender', BridgeProvider: 'Azure', BridgeCall: 'list', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'pricings', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Defender', + BridgeCollectionService: 'pricings', DataIdentifier: 'data', + }, + { + enabled: true, isSingleSource: true, InvAsset: 'defender', InvService: 'defender', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Defender Settings', BridgeServiceName: 'securitycenter', + BridgePluginCategoryName: 'Defender', BridgeProvider: 'Azure', BridgeCall: 'list', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'settings', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Defender', + BridgeCollectionService: 'securitycenter', DataIdentifier: 'data', + } + ], + 'Application Gateway': [ + { + enabled: true, isSingleSource: true, InvAsset: 'applicationGateway', InvService: 'applicationGateway', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Application Gateway', BridgeServiceName: 'applicationgateway', + BridgePluginCategoryName: 'Application Gateway', BridgeProvider: 'Azure', BridgeCall: 'listAll', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'applicationGateways', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Application Gateway', + BridgeCollectionService: 'applicationgateway', DataIdentifier: 'data', + }, + { + enabled: true, isSingleSource: true, InvAsset: 'policy', InvService: 'applicationGateway', + InvResourceCategory: 'cloud_resources', InvResourceType: 'wafpolicies', BridgeServiceName: 'wafpolicies', + BridgePluginCategoryName: 'Application Gateway', BridgeProvider: 'Azure', BridgeCall: 'listAll', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'ApplicationGatewayWebApplicationFirewallPolicies', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Application Gateway', + BridgeCollectionService: 'wafpolicies', DataIdentifier: 'data', + } + ], + 'Entra ID': [ + { + enabled: true, isSingleSource: true, InvAsset: 'entraId', InvService: 'entraId', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Roles', BridgeServiceName: 'roledefinitions', + BridgePluginCategoryName: 'Entra ID', BridgeProvider: 'Azure', BridgeCall: 'list', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: 'roleDefinitions', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Entra ID', + BridgeCollectionService: 'roledefinitions', DataIdentifier: 'data', + }, + { + enabled: true, isSingleSource: true, InvAsset: 'entraId', InvService: 'entraId', + InvResourceCategory: 'cloud_resources', InvResourceType: 'Application', BridgeServiceName: 'applications', + BridgePluginCategoryName: 'Entra ID', BridgeProvider: 'Azure', BridgeCall: 'list', + BridgeArnIdentifier: '', BridgeIdTemplate: '', BridgeResourceType: '', + BridgeResourceNameIdentifier: 'name', BridgeExecutionService: 'Entra ID', + BridgeCollectionService: 'applications', DataIdentifier: 'data', + } + ] }; // Standard calls that contain top-level operations @@ -215,7 +323,7 @@ var calls = { listAll: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Network/virtualNetworks?api-version=2020-03-01' }, - sendIntegration: serviceMap['Virtual Networks'] + sendIntegration: serviceMap['Virtual Networks'][0] }, natGateways: { listBySubscription: { @@ -267,7 +375,7 @@ var calls = { }, vaults: { list: { - url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.KeyVault/vaults?api-version=2019-09-01' + url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.KeyVault/vaults?api-version=2023-07-01' }, sendIntegration: serviceMap['Key Vaults'], }, @@ -290,7 +398,8 @@ var calls = { routeTables: { listAll: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Network/routeTables?api-version=2022-07-01' - } + }, + sendIntegration: serviceMap['Virtual Networks'][1] }, managedClusters: { list: { @@ -338,7 +447,7 @@ var calls = { list: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Cdn/profiles?api-version=2024-02-01' }, - sendIntegration: serviceMap['CDN Profiles'] + sendIntegration: serviceMap['CDN Profiles'][0] }, autoProvisioningSettings: { list: { @@ -348,7 +457,8 @@ var calls = { applicationGateway: { listAll: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Network/applicationGateways?api-version=2022-07-01' - } + }, + sendIntegration: serviceMap['Application Gateway'][0] }, securityContacts: { list: { @@ -375,7 +485,8 @@ var calls = { roleDefinitions: { list: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/roleDefinitions?api-version=2015-07-01' - } + }, + sendIntegration: serviceMap['Entra ID'][0] }, managementLocks: { listAtSubscriptionLevel: { @@ -407,7 +518,8 @@ var calls = { list: { url: 'https://graph.microsoft.com/v1.0/applications/', graph: true, - } + }, + sendIntegration: serviceMap['Entra ID'][1] }, automationAccounts: { list: { @@ -422,7 +534,8 @@ var calls = { pricings: { list: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Security/pricings?api-version=2018-06-01' - } + }, + sendIntegration: serviceMap['Defender'][0] }, availabilitySets: { listBySubscription: { @@ -432,7 +545,8 @@ var calls = { virtualMachineScaleSets: { listAll: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Compute/virtualMachineScaleSets?api-version=2023-07-01' - } + }, + sendIntegration: serviceMap['Virtual Machines'] }, bastionHosts: { listAll: { @@ -442,7 +556,8 @@ var calls = { wafPolicies: { listAll: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies?api-version=2022-07-01' - } + }, + sendIntegration: serviceMap['Application Gateway'][1] }, autoscaleSettings: { listBySubscription: { @@ -462,7 +577,7 @@ var calls = { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.DBforMySQL/servers?api-version=2017-12-01' }, listMysqlFlexibleServer: { - url : 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.DBforMySQL/flexibleServers?api-version=2021-05-01' + url : 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.DBforMySQL/flexibleServers?api-version=2023-12-30' }, listPostgres: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.DBforPostgreSQL/servers?api-version=2017-12-01' @@ -480,7 +595,8 @@ var calls = { securityCenter: { list: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Security/settings?api-version=2021-06-01' - } + }, + sendIntegration: serviceMap['Defender'][1] }, publicIPAddresses: { listAll: { @@ -500,12 +616,14 @@ var calls = { eventGrid: { listDomains: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.EventGrid/domains?api-version=2023-12-15-preview' - } + }, + sendIntegration: serviceMap['Event Grid'] }, eventHub: { listEventHub: { url: 'https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.EventHub/namespaces?api-version=2022-10-01-preview' - } + }, + sendIntegration: serviceMap['Event Hubs'] }, serviceBus: { listNamespacesBySubscription: { @@ -815,7 +933,8 @@ var postcalls = { url: 'https://management.azure.com/{id}/blobServices/default/containers?api-version=2019-06-01', rateLimit: 3000, limit: 20000 - } + }, + sendIntegration: serviceMap['Blob Service'] }, blobServices: { list: { @@ -891,6 +1010,14 @@ var postcalls = { properties: ['id'], url: 'https://management.azure.com/{id}/config/backup/list?api-version=2021-02-01', post: true + }, + getWebAppDetails: { + reliesOnPath: 'webApps.list', + properties: ['id'], + url: 'https://management.azure.com/{id}?api-version=2022-03-01' + }, + sendIntegration: { + enabled: true } }, containerApps: { @@ -905,7 +1032,8 @@ var postcalls = { reliesOnPath: 'profiles.list', properties: ['id'], url: 'https://management.azure.com/{id}/endpoints?api-version=2019-04-15' - } + }, + sendIntegration: serviceMap['CDN Profiles'][1] }, customDomain: { listByFrontDoorProfiles: { @@ -976,6 +1104,11 @@ var postcalls = { reliesOnPath: 'servers.listPostgresFlexibleServer', properties: ['id'], url: 'https://management.azure.com/{id}/firewallRules?api-version=2022-12-01' + }, + listByFlexibleServerMysql: { + reliesOnPath: 'servers.listMysqlFlexibleServer', + properties: ['id'], + url: 'https://management.azure.com/{id}/firewallRules?api-version=2021-05-01' } }, outboundFirewallRules: { @@ -1131,6 +1264,13 @@ var postcalls = { url: 'https://management.azure.com/{id}/encryptionScopes?api-version=2023-01-01' } }, + eventHub: { + listNetworkRuleSet: { + reliesOnPath: 'eventHub.listEventHub', + properties: ['id'], + url: 'https://management.azure.com/{id}/networkRuleSets/default?api-version=2022-10-01-preview' + } + } }; var tertiarycalls = { @@ -1428,4 +1568,4 @@ module.exports = { tertiarycalls: tertiarycalls, specialcalls: specialcalls, serviceMap: serviceMap -}; +}; \ No newline at end of file diff --git a/helpers/azure/auth.js b/helpers/azure/auth.js index d4ad17a45..992794134 100644 --- a/helpers/azure/auth.js +++ b/helpers/azure/auth.js @@ -1,5 +1,5 @@ -var request = require('request'); var locations = require(__dirname + '/locations.js'); +var axios = require('axios'); var locations_gov = require(__dirname + '/locations_gov.js'); var dontReplace = { @@ -36,57 +36,66 @@ module.exports = { if (!azureConfig.KeyValue) return callback('No KeyValue provided'); if (!azureConfig.DirectoryID) return callback('No DirectoryID provided'); if (!azureConfig.SubscriptionID) return callback('No SubscriptionID provided'); + var { ClientSecretCredential } = require('@azure/identity'); - var msRestAzure = require('ms-rest-azure'); - - function performLogin(tokenAudience, cb) { - msRestAzure.loginWithServicePrincipalSecret( - azureConfig.ApplicationID, - azureConfig.KeyValue, - azureConfig.DirectoryID, - tokenAudience, function(err, credentials) { - if (err) return cb(err); - if (!credentials) return cb('Unable to log into Azure using provided credentials.'); - if (!credentials.environment) return cb('Unable to obtain environment from Azure application'); - if (!credentials.tokenCache || - !credentials.tokenCache._entries || - !credentials.tokenCache._entries[0] || - !credentials.tokenCache._entries[0].accessToken) { - return cb('Unable to obtain token from Azure.'); - } - - cb(null, credentials); + function getToken(credential, scopes, cb) { + credential.getToken(scopes) + .then(response => { + cb(null, response.token); + }) + .catch(error => { + cb(error); }); } + const credential = new ClientSecretCredential( + azureConfig.DirectoryID, + azureConfig.ApplicationID, + azureConfig.KeyValue + ); + if (azureConfig.Govcloud) { - performLogin({ environment: msRestAzure.AzureEnvironment.AzureUSGovernment }, function(err, credentials) { + const armScope = 'https://management.usgovcloudapi.net/.default'; + const graphScope = 'https://graph.microsoft.us/.default'; + const vaultScope = 'https://vault.azure.us/.default'; + + getToken(credential, [armScope], function(err, armToken) { if (err) return callback(err); - performLogin({ tokenAudience: 'https://graph.microsoft.us', environment: msRestAzure.AzureEnvironment.AzureUSGovernment }, function(graphErr, graphCredentials) { + getToken(credential, [graphScope], function(graphErr, graphToken) { if (graphErr) return callback(graphErr); - performLogin({ tokenAudience: 'https://vault.azure.us', environment: msRestAzure.AzureEnvironment.AzureUSGovernment }, function(vaultErr, vaultCredentials) { + getToken(credential, [vaultScope], function(vaultErr, vaultToken) { if (vaultErr) console.log('No vault'); callback(null, { - environment: credentials.environment, - token: credentials.tokenCache._entries[0].accessToken, - graphToken: graphCredentials ? graphCredentials.tokenCache._entries[0].accessToken : null, - vaultToken: vaultCredentials ? vaultCredentials.tokenCache._entries[0].accessToken : null + environment: { + name: 'AzureUSGovernment', + portalUrl: 'https://portal.azure.us' + }, + token: armToken, + graphToken: graphToken, + vaultToken: vaultToken }); }); }); }); } else { - performLogin(null, function(err, credentials) { + const armScope = 'https://management.azure.com/.default'; + const graphScope = 'https://graph.microsoft.com/.default'; + const vaultScope = 'https://vault.azure.net/.default'; + + getToken(credential, [armScope], function(err, armToken) { if (err) return callback(err); - performLogin({ tokenAudience: 'https://graph.microsoft.com' }, function(graphErr, graphCredentials) { + getToken(credential, [graphScope], function(graphErr, graphToken) { if (graphErr) return callback(graphErr); - performLogin({ tokenAudience: 'https://vault.azure.net' }, function(vaultErr, vaultCredentials) { + getToken(credential, [vaultScope], function(vaultErr, vaultToken) { if (vaultErr) return callback(vaultErr); callback(null, { - environment: credentials.environment, - token: credentials.tokenCache._entries[0].accessToken, - graphToken: graphCredentials.tokenCache._entries[0].accessToken, - vaultToken: vaultCredentials.tokenCache._entries[0].accessToken + environment: { + name: 'AzureCloud', + portalUrl: 'https://portal.azure.com' + }, + token: armToken, + graphToken: graphToken, + vaultToken: vaultToken }); }); }); @@ -99,89 +108,162 @@ module.exports = { 'Authorization': `Bearer ${params.token}` }; + var requestData = null; if (params.body && Object.keys(params.body).length) { - headers['Content-Length'] = JSON.stringify(params.body).length; + requestData = JSON.stringify(params.body); + headers['Content-Length'] = requestData.length; headers['Content-Type'] = 'application/json;charset=UTF-8'; } if (params.govcloud) params.url = params.url.replace('management.azure.com', 'management.usgovcloudapi.net'); - - request({ + var axiosOptions = { method: params.method ? params.method : params.post ? 'POST' : 'GET', - uri: params.url, + url: params.url, headers: headers, - body: params.body ? JSON.stringify(params.body) : null - }, function(error, response, body) { - if (response && [200, 202].includes(response.statusCode) && body) { + data: requestData, + // Handle response as text first, then parse manually to match original behavior + transformResponse: [(data) => data] + }; + + axios(axiosOptions) + .then(function(response) { + var body = response.data; + + if (response && [200, 202].includes(response.status) && body) { + try { + body = JSON.parse(body); + } catch (e) { + return callback(`Error parsing response from Azure API: ${e}`); + } + return callback(null, body); + } else { + handleErrorResponse(body, response, callback); + } + }) + .catch(function(error) { + if (error.response) { + // The request was made and the server responded with a status code outside 2xx + handleErrorResponse(error.response.data, error.response, callback); + } else if (error.request) { + // The request was made but no response was received + if (error.code === 'ECONNRESET') { + console.log('[ERROR] Unhandled error from Azure API: Error: ECONNRESET'); + return callback('Unknown error occurred while calling the Azure API: ECONNRESET'); + } + console.log(`[ERROR] Unhandled error from Azure API: Error: ${error}`); + return callback('Unknown error occurred while calling the Azure API'); + } else { + // Something happened in setting up the request + console.log(`[ERROR] Unhandled error from Azure API: Error: ${error}`); + return callback('Unknown error occurred while calling the Azure API'); + } + }); + + function handleErrorResponse(body, response, callback) { + if (body) { try { body = JSON.parse(body); } catch (e) { - return callback(`Error parsing response from Azure API: ${e}`); + return callback(`Error parsing error response from Azure API: ${e}`); } - return callback(null, body); - } else { - if (body) { + + if (typeof body == 'string') { + // Need to double parse it try { body = JSON.parse(body); } catch (e) { - return callback(`Error parsing error response from Azure API: ${e}`); + return callback(`Error parsing error response string from Azure API: ${e}`); } + } - if (typeof body == 'string') { - // Need to double parse it - try { - body = JSON.parse(body); - } catch (e) { - return callback(`Error parsing error response string from Azure API: ${e}`); - } + if (response && ((response.statusCode && response.statusCode === 429) || (response.status && response.status === 429)) && + body && + body.error && + body.error.message && + typeof body.error.message == 'string') { + var errorMessage = `TooManyRequests: ${body.error.message}`; + return callback(errorMessage, null, response); + } else if (body && + body.error && + body.error.message && + typeof body.error.message == 'string') { + return callback(body.error.message); + } else if (body && + body['odata.error'] && + body['odata.error'].message && + body['odata.error'].message.value && + typeof body['odata.error'].message.value == 'string') { + if (body['odata.error'].requestId) { + body['odata.error'].message.value += ` RequestId: ${body['odata.error'].requestId}`; } - if (response && - response.statusCode && - response.statusCode === 429 && - body && - body.error && - body.error.message && - typeof body.error.message == 'string') { - var errorMessage = `TooManyRequests: ${body.error.message}`; - return callback(errorMessage, null, response); - } else if (body && - body.error && - body.error.message && - typeof body.error.message == 'string') { - return callback(body.error.message); - } else if (body && - body['odata.error'] && - body['odata.error'].message && - body['odata.error'].message.value && - typeof body['odata.error'].message.value == 'string') { - if (body['odata.error'].requestId) { - body['odata.error'].message.value += ` RequestId: ${body['odata.error'].requestId}`; - } - return callback(body['odata.error'].message.value); - } else if (body && - body.message && - typeof body.message == 'string') { - if (body.code && typeof body.code == 'string') { - body.message = (body.code + ': ' + body.message); - } - return callback(body.message); - } else if (body && - body.Message && - typeof body.Message == 'string') { - if (body.Code && typeof body.Code == 'string') { - body.Message = (body.Code + ': ' + body.Message); - } - return callback(body.Message); + return callback(body['odata.error'].message.value); + } else if (body && + body.message && + typeof body.message == 'string') { + if (body.code && typeof body.code == 'string') { + body.message = (body.code + ': ' + body.message); + } + return callback(body.message); + } else if (body && + body.Message && + typeof body.Message == 'string') { + if (body.Code && typeof body.Code == 'string') { + body.Message = (body.Code + ': ' + body.Message); + } + return callback(body.Message); + } + if (typeof body == 'string') { + // Need to double parse it + try { + body = JSON.parse(body); + } catch (e) { + return callback(`Error parsing error response string from Azure API: ${e}`); } - - console.log(`[ERROR] Unhandled error from Azure API: Body: ${JSON.stringify(body)}`); + } + if (response && ((response.statusCode && response.statusCode === 429) || (response.status && response.status === 429)) && + body && + body.error && + body.error.message && + typeof body.error.message == 'string') { + errorMessage = `TooManyRequests: ${body.error.message}`; + return callback(errorMessage, null, response); + } else if (body && + body.error && + body.error.message && + typeof body.error.message == 'string') { + return callback(body.error.message); + } else if (body && + body['odata.error'] && + body['odata.error'].message && + body['odata.error'].message.value && + typeof body['odata.error'].message.value == 'string') { + if (body['odata.error'].requestId) { + body['odata.error'].message.value += ` RequestId: ${body['odata.error'].requestId}`; + } + return callback(body['odata.error'].message.value); + } else if (body && + body.message && + typeof body.message == 'string') { + if (body.code && typeof body.code == 'string') { + body.message = (body.code + ': ' + body.message); + } + return callback(body.message); + } else if (body && + body.Message && + typeof body.Message == 'string') { + if (body.Code && typeof body.Code == 'string') { + body.Message = (body.Code + ': ' + body.Message); + } + return callback(body.Message); } - console.log(`[ERROR] Unhandled error from Azure API: Error: ${error}`); - return callback('Unknown error occurred while calling the Azure API'); + console.log(`[ERROR] Unhandled error from Azure API: Body: ${JSON.stringify(body)}`); } - }); + + console.log('[ERROR] Unhandled error from Azure API'); + return callback('Unknown error occurred while calling the Azure API'); + } }, addLocations: function(obj, service, collection, err, data, skip_locations) { @@ -207,7 +289,8 @@ module.exports = { } }); }, - addGovLocations: function(obj, service, collection, err, data , skip_locations) { + + addGovLocations: function(obj, service, collection, err, data, skip_locations) { if (!service || !locations_gov[service]) return; locations_gov[service].forEach(function(location) { if (skip_locations.includes(location)) return; @@ -232,5 +315,4 @@ module.exports = { }, reduceProperties: reduceProperties -}; - +}; \ No newline at end of file diff --git a/helpers/shared.js b/helpers/shared.js index 1222c84eb..beeeb4e23 100644 --- a/helpers/shared.js +++ b/helpers/shared.js @@ -3,7 +3,6 @@ var async = require('async'); var ONE_DAY = 24*60*60*1000; var ONE_HOUR = 60*60*1000; -let identityServices = ['iam', 'aad']; var daysBetween = function(date1, date2) { return Math.round(Math.abs((new Date(date1).getTime() - new Date(date2).getTime())/(ONE_DAY))); }; @@ -21,11 +20,6 @@ var processIntegration = function(serviceName, settings, collection, calls, post let localSettings = {}; localSettings = settings; - if (settings.identifier.new_inventory_enabled && - (identityServices.includes(serviceName.toLowerCase()) || (calls[serviceName] && calls[serviceName].sendIntegration && calls[serviceName].sendIntegration.isIdentity))) { - console.log(`Not sending ${serviceName} because new inventory ff is enabled`); - return iCb(); - } if (settings.govcloud) { localEvent.awsOrGov = 'aws-us-gov'; } @@ -33,7 +27,7 @@ var processIntegration = function(serviceName, settings, collection, calls, post localEvent.scanTriggeredFromEventsFlow = settings.scanTriggeredFromEventsFlow; localEvent.collection = {}; localEvent.previousCollection = {}; - + localEvent.cloud_account_identifier = settings.identifier.cloud_account_identifier; localEvent.lastScanId = settings.lastScanId; localEvent.collection[serviceName.toLowerCase()] = {}; @@ -433,5 +427,4 @@ module.exports = { } return 0; } -}; - +}; \ No newline at end of file diff --git a/index.js b/index.js index 65230eb16..8ab5f23cf 100755 --- a/index.js +++ b/index.js @@ -14,7 +14,7 @@ console.log(` | | |_| - CloudSploit by Khulnasoft Security, Ltd. + CloudExploit by Khulnasoft Security, Ltd. Cloud security auditing for AWS, Azure, GCP, Oracle, and GitHub `); diff --git a/package.json b/package.json index ad99d5e59..3534fe93b 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,24 @@ { "name": "cloudexploit", - "version": "2.0.1", + "version": "2.1.0", "description": "AWS, Azure, GCP, Oracle, GitHub security scanning scripts", "main": "index.js", + "type": "module", + "engines": { + "node": ">=16.0.0" + }, "scripts": { - "test": "mocha './**/*.spec.js'", - "test-watch": "nodemon --exec npm run test", - "test-cov": "nyc --reporter text --reporter cobertura --reporter lcov --all -- npm run test", - "test-cov-watch": "nodemon --exec npm run test-cov", - "test-cov-html": "nyc --reporter html --reporter cobertura --reporter lcov --all -- npm run test", - "test-cov-html-watch": "nodemon --exec npm run test-cov-html", - "lint": "npx eslint ." + "start": "node index.js", + "test": "NODE_ENV=test mocha './**/*.spec.js' --timeout 10000 --exit", + "test:watch": "nodemon --exec 'npm test' --ext 'js,json'", + "test:coverage": "nyc --reporter=html --reporter=text --reporter=lcov mocha './**/*.spec.js' --timeout 10000", + "test:ci": "npm run test -- --reporter mocha-junit-reporter --reporter-options mochaFile=./test-results.xml", + "lint": "eslint . --ext .js", + "lint:fix": "eslint . --ext .js --fix", + "audit": "npm audit --production --audit-level=moderate", + "security-check": "npm audit --production && npx snyk test", + "precommit": "npm run lint && npm test", + "prepare": "husky install" }, "bin": { "cloudexploit-scan": "./index.js" @@ -39,31 +47,73 @@ "access": "public" }, "dependencies": { - "@alicloud/pop-core": "^1.7.10", - "@azure/data-tables": "^13.2.2", - "@azure/storage-file-share": "^12.14.0", - "@azure/storage-queue": "^12.13.0", - "@azure/storage-blob": "^12.14.0", - "@octokit/auth-app": "^6.0.3", - "@octokit/request": "^8.1.6", - "@octokit/rest": "^20.0.2", - "ali-oss": "^6.15.2", - "argparse": "^2.0.0", - "async": "^2.6.1", - "aws-sdk": "^2.1506.0", - "azure-storage": "^2.10.3", + "@alicloud/pop-core": "^1.7.11", + "@apidevtools/json-schema-ref-parser": "^10.1.0", + "@azure/identity": "^4.0.0", + "@azure/storage-blob": "^12.17.0", + "@azure/storage-file-share": "^12.19.0", + "@azure/storage-queue": "^12.19.0", + "@azure/data-tables": "^13.3.2", + "@octokit/auth-app": "^7.0.0", + "@octokit/rest": "^21.0.0", + "ali-oss": "^6.17.0", + "argparse": "^2.0.1", + "async": "^3.2.5", + "aws-sdk": "^2.1574.0", + "axios": "^1.6.5", + "chalk": "^4.1.2", + "commander": "^11.1.0", "csv-write-stream": "^2.0.0", - "fast-safe-stringify": "^2.0.6", - "google-auth-library": "^8.1.1", - "minimatch": "^3.0.4", + "dotenv": "^16.3.1", + "fast-safe-stringify": "^2.1.1", + "google-auth-library": "^9.9.0", + "js-yaml": "^4.1.0", + "minimatch": "^9.0.3", "ms-rest-azure": "^3.0.2", - "tty-table": "^4.1.3" + "ora": "^5.4.1", + "pino": "^8.17.1", + "pino-pretty": "^10.2.3", + "tty-table": "^4.1.3", + "winston": "^3.11.0" }, "devDependencies": { - "chai": "4.2.0", - "eslint": "^6.8.0", - "mocha": "^6.1.4", - "nodemon": "^1.19.4", - "nyc": "^14.1.1" + "@babel/core": "^7.23.5", + "@babel/eslint-parser": "^7.23.3", + "@babel/preset-env": "^7.23.5", + "chai": "^4.3.10", + "eslint": "^8.56.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsdoc": "^46.9.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-security": "^3.0.1", + "husky": "^9.0.5", + "lint-staged": "^15.2.0", + "mocha": "^10.2.0", + "mocha-junit-reporter": "^2.2.0", + "nock": "^13.3.3", + "nodemon": "^3.0.2", + "nyc": "^15.1.0", + "prettier": "^3.1.1", + "sinon": "^17.0.0", + "snyk": "^1.1252.0" + }, + "lint-staged": { + "*.js": [ + "eslint --fix", + "prettier --write" + ] + }, + "prettier": { + "semi": true, + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "es5", + "bracketSpacing": true, + "arrowParens": "avoid" } } diff --git a/plugins/aws/ec2/ebsRecentSnapshots.js b/plugins/aws/ec2/ebsRecentSnapshots.js index 4e8d9016c..a0ee149f1 100644 --- a/plugins/aws/ec2/ebsRecentSnapshots.js +++ b/plugins/aws/ec2/ebsRecentSnapshots.js @@ -6,14 +6,26 @@ module.exports = { category: 'EC2', domain: 'Compute', severity: 'Medium', - description: 'Ensures that EBS volume has had a snapshot within the last 7 days', + description: 'Ensures that EBS volume has had a recent snapshot within the configured time period', more_info: 'EBS volumes without recent snapshots may be at risk of data loss or recovery issues.', link: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSSnapshots.html', - recommended_action: 'Create a new snapshot for EBS volume weekly.', + recommended_action: 'Create a new snapshot for EBS volume within the configured time period.', apis: ['EC2:describeSnapshots','STS:getCallerIdentity'], + settings: { + ebs_recent_snapshot_days: { + name: 'EBS Recent Snapshot Days', + description: 'Number of days to consider a snapshot as recent. Snapshots older than this will be flagged as FAIL.', + regex: '^[1-9]{1}[0-9]{0,2}$', + default: '7' + } + }, realtime_triggers: ['ec2:CreateSnapshot', 'ec2:DeleteSnapshot'], run: function(cache, settings, callback) { + var config = { + ebs_recent_snapshot_days: parseInt(settings.ebs_recent_snapshot_days || this.settings.ebs_recent_snapshot_days.default) + }; + var results = []; var source = {}; var regions = helpers.regions(settings); @@ -44,7 +56,7 @@ module.exports = { var snapshotTime = new Date(snapshot.StartTime); var difference = Math.floor((today -snapshotTime) / (1000 * 60 * 60 * 24)); - if (difference > 7){ + if (difference > config.ebs_recent_snapshot_days){ helpers.addResult(results, 2, 'EBS volume does not have a recent snapshot', region,resource); } else { @@ -58,5 +70,4 @@ module.exports = { callback(null, results, source); }); } -}; - +}; \ No newline at end of file diff --git a/plugins/aws/ec2/ebsRecentSnapshots.spec.js b/plugins/aws/ec2/ebsRecentSnapshots.spec.js index 686316e9b..0f4bd7dcb 100644 --- a/plugins/aws/ec2/ebsRecentSnapshots.spec.js +++ b/plugins/aws/ec2/ebsRecentSnapshots.spec.js @@ -7,6 +7,9 @@ snapshotPass.setDate(snapshotPass.getDate() - 1); var snapshotFail = new Date(); snapshotFail.setDate(snapshotFail.getDate() - 10); +var snapshotCustom = new Date(); +snapshotCustom.setDate(snapshotCustom.getDate() - 15); + const describeSnapshots = [ { "Description": "", @@ -47,6 +50,18 @@ const describeSnapshots = [ "VolumeId": "vol-02c402f5a6a02c6e7", "VolumeSize": 1, "Tags": [] + }, + { + "Description": "Custom test snapshot", + "Encrypted": false, + "OwnerId": "112233445566", + "Progress": "100%", + "SnapshotId": "snap-04custom567890abc", + "StartTime": snapshotCustom, + "State": "completed", + "VolumeId": "vol-03custom567890def", + "VolumeSize": 10, + "Tags": [] } ]; @@ -135,5 +150,40 @@ describe('ebsRecentSnapshots', function () { done(); }); }); + + it('should use custom snapshot age threshold when setting is provided', function (done) { + const cache = createCache([describeSnapshots[3]]); // 15-day old snapshot + const settings = { ebs_recent_snapshot_days: '20' }; + ebsRecentSnapshots.run(cache, settings, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('EBS volume has a recent snapshot'); + done(); + }); + }); + + it('should FAIL when snapshot is older than custom threshold', function (done) { + const cache = createCache([describeSnapshots[3]]); // 15-day old snapshot + const settings = { ebs_recent_snapshot_days: '10' }; + ebsRecentSnapshots.run(cache, settings, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('EBS volume does not have a recent snapshot'); + done(); + }); + }); + + it('should use default 7 days when no setting is provided', function (done) { + const cache = createCache([describeSnapshots[1]]); // 10-day old snapshot + ebsRecentSnapshots.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].region).to.equal('us-east-1'); + expect(results[0].message).to.include('EBS volume does not have a recent snapshot'); + done(); + }); + }); }); }); \ No newline at end of file diff --git a/plugins/aws/kms/kmsDefaultKeyUsage.js b/plugins/aws/kms/kmsDefaultKeyUsage.js index 8a309c865..495613bb0 100644 --- a/plugins/aws/kms/kmsDefaultKeyUsage.js +++ b/plugins/aws/kms/kmsDefaultKeyUsage.js @@ -11,7 +11,7 @@ module.exports = { link: 'http://docs.aws.amazon.com/kms/latest/developerguide/concepts.html', recommended_action: 'Avoid using the default KMS key', apis: ['KMS:listKeys', 'KMS:describeKey', 'KMS:listAliases', 'CloudTrail:describeTrails', - 'EC2:describeVolumes', 'ElasticTranscoder:listPipelines', 'RDS:describeDBInstances', + 'EC2:describeVolumes', 'RDS:describeDBInstances', 'Redshift:describeClusters', 'S3:listBuckets', 'S3:getBucketEncryption', 'SES:describeActiveReceiptRuleSet', 'Workspaces:describeWorkspaces', 'Lambda:listFunctions', 'CloudWatchLogs:describeLogGroups', 'EFS:describeFileSystems', 'STS:getCallerIdentity'], @@ -21,7 +21,7 @@ module.exports = { 'passwords, it is still strongly encouraged to use a ' + 'customer-provided CMK rather than the default KMS key.' }, - realtime_triggers: ['cloudtrail:CreateTrail','cloudtrail:UpdateTrail','cloudtrail:DeleteTrail','ec2:CreateVolume','ec2:DeleteVolume','elastictranscoder:UpdatePipeline','elastictranscoder:CreatePipeline','elastictranscoder:DeletePipeline','rds:CreateDBInstance','rds:ModifyDBInstance','rds:DeleteDBInstance','redshift:CreateCluster','redshift:ModifyCluster','redshift:DeleteCluster','s3:CreateBucket','s3:DeleteBucket','s3:PutBucketEncryption','ses:CreateReceiptRule','ses:DeleteReceiptRule','ses:UpdateReceiptRule','workspaces:CreateWorkspaces','workspaces:TerminateWorkspaces','lambda:UpdateFunctionConfiguration','lambda:CreateFunction','lambda:DeleteFunction','cloudwatchlogs:CreateLogGroup','cloudwatchlogs:DeleteLogGroup','cloudwatchlogs:AssociateKmsKey','efs:CreateFileSystem',':efs:DeleteFileSystem'], + realtime_triggers: ['cloudtrail:CreateTrail','cloudtrail:UpdateTrail','cloudtrail:DeleteTrail','ec2:CreateVolume','ec2:DeleteVolume','rds:CreateDBInstance','rds:ModifyDBInstance','rds:DeleteDBInstance','redshift:CreateCluster','redshift:ModifyCluster','redshift:DeleteCluster','s3:CreateBucket','s3:DeleteBucket','s3:PutBucketEncryption','ses:CreateReceiptRule','ses:DeleteReceiptRule','ses:UpdateReceiptRule','workspaces:CreateWorkspaces','workspaces:TerminateWorkspaces','lambda:UpdateFunctionConfiguration','lambda:CreateFunction','lambda:DeleteFunction','cloudwatchlogs:CreateLogGroup','cloudwatchlogs:DeleteLogGroup','cloudwatchlogs:AssociateKmsKey','efs:CreateFileSystem',':efs:DeleteFileSystem'], run: function(cache, settings, callback) { var results = []; @@ -100,28 +100,6 @@ module.exports = { } } } - } - - // For ElasticTranscoder - if (region in regions.elastictranscoder) { - var listPipelines = helpers.addSource(cache, source, ['elastictranscoder', 'listPipelines', region]); - - if (listPipelines) { - if (listPipelines.err || !listPipelines.data) { - helpers.addResult(results, 3, - 'Unable to query for ElasticTranscoder pipelines: ' + helpers.addError(listPipelines), region); - } else { - for (var k in listPipelines.data){ - if (listPipelines.data[k].AwsKmsKeyArn) { - services.push({ - serviceName: 'ElasticTranscoder', - resource: listPipelines.data[k].Arn, - KMSKey: listPipelines.data[k].AwsKmsKeyArn - }); - } - } - } - } } // For RDS diff --git a/plugins/aws/s3/bucketSecureTransportEnabled.spec.js b/plugins/aws/s3/bucketSecureTransportEnabled.spec.js index 7694f8597..dbbf055e9 100644 --- a/plugins/aws/s3/bucketSecureTransportEnabled.spec.js +++ b/plugins/aws/s3/bucketSecureTransportEnabled.spec.js @@ -21,7 +21,7 @@ const getBucketPolicy = [ Policy: '{"Version":"2012-10-17","Id":"ExamplePolicy","Statement":[{"Sid":"","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::00000011111:root"},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::staging-01-sd-logs/*"]},{"Sid":"","Effect":"Deny","Principal":"*","Action":"s3:*","Resource":["arn:aws:s3:::staging-01-sd-logs/*","arn:aws:s3:::staging-01-sd-logs"],"Condition":{"Bool":{"aws:SecureTransport":"false"}}}]}' }, { - Policy: '{"Version":"2008-10-17","Statement":[{"Sid":"Stmt1537431944913","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::00001111122:root"},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::alqemy-upwork/*"]},{"Sid":"Stmt1537431944211","Effect":"Deny","Principal":"*","Action":"s3:*","Resource":["arn:aws:s3:::alqemy-upwork/*","arn:aws:s3:::alqemy-upwork"],"Condition":{"Bool":{"aws:SecureTransport":"false"}}}]}' + Policy: '{"Version":"2008-10-17","Statement":[{"Sid":"Stmt1537431944913","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::00001111122:root"},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::cloudexploit-test-secure-transport/*"]},{"Sid":"Stmt1537431944211","Effect":"Deny","Principal":"*","Action":"s3:*","Resource":["arn:aws:s3:::cloudexploit-test-secure-transport/*","arn:aws:s3:::cloudexploit-test-secure-transport"],"Condition":{"Bool":{"aws:SecureTransport":"false"}}}]}' }, { Policy: '{"Version":"2012-10-17","Id":"ExamplePolicy","Statement":[]}' diff --git a/plugins/aws/s3/s3BucketHasTags.js b/plugins/aws/s3/s3BucketHasTags.js index d225153de..26ed31210 100644 --- a/plugins/aws/s3/s3BucketHasTags.js +++ b/plugins/aws/s3/s3BucketHasTags.js @@ -9,7 +9,7 @@ module.exports = { more_info: 'Tags help you to group resources together that are related to or associated with each other. It is a best practice to tag cloud resources to better organize and gain visibility into their usage.', recommended_action: 'Modify S3 buckets and add tags.', link: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/CostAllocTagging.html', - apis: ['S3:listBuckets', 'ResourceGroupsTaggingAPI:getResources', 'S3:getBucketLocation'], + apis: ['S3:listBuckets', 'S3:getBucketTagging', 'S3:getBucketLocation'], realtime_triggers: ['s3:CreateBucket', 's3:PutBucketTagging','s3:DeleteBucket'], run: function(cache, settings, callback) { @@ -32,20 +32,43 @@ module.exports = { return callback(null, results, source); } - var bucketsByRegion= {}; listBuckets.data.forEach(function(bucket) { if (!bucket.Name) return; + var bucketLocation = helpers.getS3BucketLocation(cache, defaultRegion, bucket.Name); - if (!bucketsByRegion[bucketLocation]) { - bucketsByRegion[bucketLocation] = []; + var bucketArn = `arn:${awsOrGov}:s3:::${bucket.Name}`; + + // Try the bucket's actual region first, then fall back to default region + var getBucketTagging = helpers.addSource(cache, source, + ['s3', 'getBucketTagging', bucketLocation, bucket.Name]); + + // If not found in bucket's region, try default region (where collector runs) + if (!getBucketTagging) { + getBucketTagging = helpers.addSource(cache, source, + ['s3', 'getBucketTagging', defaultRegion, bucket.Name]); + } + + + if (!getBucketTagging || getBucketTagging.err) { + if (getBucketTagging && getBucketTagging.err && + (getBucketTagging.err.code === 'NoSuchTagSet' || + getBucketTagging.err.message && getBucketTagging.err.message.includes('does not exist'))) { + // No tags exist for this bucket + helpers.addResult(results, 2, 'S3 bucket does not have any tags', bucketLocation, bucketArn); + } else { + helpers.addResult(results, 3, + 'Unable to query S3 bucket tags: ' + helpers.addError(getBucketTagging), + bucketLocation, bucketArn); + } + return; + } + + if (getBucketTagging.data && getBucketTagging.data.TagSet && getBucketTagging.data.TagSet.length > 0) { + helpers.addResult(results, 0, 'S3 bucket has tags', bucketLocation, bucketArn); + } else { + helpers.addResult(results, 2, 'S3 bucket does not have any tags', bucketLocation, bucketArn); } - bucketsByRegion[bucketLocation].push(`arn:${awsOrGov}:s3:::${bucket.Name}`); }); - - for (var region in bucketsByRegion) { - var bucketNames = bucketsByRegion[region] || []; - helpers.checkTags(cache, 'S3 bucket', bucketNames, region, results, settings); - } callback(null, results, source); } -}; +}; \ No newline at end of file diff --git a/plugins/aws/s3/s3BucketHasTags.spec.js b/plugins/aws/s3/s3BucketHasTags.spec.js index c5d16a432..5eefe1109 100644 --- a/plugins/aws/s3/s3BucketHasTags.spec.js +++ b/plugins/aws/s3/s3BucketHasTags.spec.js @@ -1,7 +1,7 @@ var expect = require('chai').expect; var s3BucketHasTags = require('./s3BucketHasTags'); -const createCache = (bucketData, bucketDataErr, rgData, rgDataErr) => { +const createCache = (bucketData, bucketDataErr, bucketTaggingData, bucketTaggingErr) => { var bucketName = (bucketData && bucketData.length) ? bucketData[0].Name : null; return { s3: { @@ -19,13 +19,13 @@ const createCache = (bucketData, bucketDataErr, rgData, rgDataErr) => { } } } - } - }, - resourcegroupstaggingapi: { - getResources: { - 'us-east-1':{ - err: rgDataErr, - data: rgData + }, + getBucketTagging: { + 'us-east-1': { + [bucketName]: { + err: bucketTaggingErr, + data: bucketTaggingData + } } } } @@ -58,19 +58,46 @@ describe('s3BucketHasTags', function () { s3BucketHasTags.run(cache, {}, callback); }); - it('should give unknown result if unable to query resource group tagging api', function (done) { + it('should give unknown result if unable to query bucket tagging', function (done) { const callback = (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(3); - expect(results[0].message).to.include('Unable to query all resources from group tagging api:'); + expect(results[0].message).to.include('Unable to query S3 bucket tags:'); done(); }; - const cache = createCache([{ - "Name": "test-bucket", - "CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)", - }], null, [],{ - message: "Unable to query for Resource group tags" - }); + // Create cache with error in both potential lookup locations (bucket region and default region) + const cache = { + s3: { + listBuckets: { + 'us-east-1': { + err: null, + data: [{ + "Name": "test-bucket", + "CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)", + }] + } + }, + getBucketLocation: { + 'us-east-1': { + 'test-bucket': { + data: { + LocationConstraint: null // us-east-1 + } + } + } + }, + getBucketTagging: { + 'us-east-1': { + 'test-bucket': { + err: { + message: "Unable to query bucket tags" + }, + data: null + } + } + } + } + }; s3BucketHasTags.run(cache, {}, callback); }); @@ -86,11 +113,13 @@ describe('s3BucketHasTags', function () { [{ "Name": "test-bucket", "CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)", - }],null, - [{ - "ResourceARN": "arn:aws:s3:::test-bucket", - "Tags": [{key:"key1", value:"value"}], - }],null + }], null, + { + "TagSet": [ + {"Key": "key1", "Value": "value1"}, + {"Key": "key2", "Value": "value2"} + ] + }, null ); s3BucketHasTags.run(cache, {}, callback); }); @@ -107,11 +136,32 @@ describe('s3BucketHasTags', function () { [{ "Name": "test-bucket", "CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)", - }],null, + }], null, + null, { + code: "NoSuchTagSet", + message: "The TagSet does not exist" + } + ); + + s3BucketHasTags.run(cache, {}, callback); + }); + + it('should give failing result if s3 has empty tag set', function (done) { + const callback = (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('S3 bucket does not have any tags'); + done(); + }; + + const cache = createCache( [{ - "ResourceARN": "arn:aws:s3:::test-bucket", - "Tags": [], - }],null + "Name": "test-bucket", + "CreationDate": "November 22, 2021, 15:51:19 (UTC+05:00)", + }], null, + { + "TagSet": [] + }, null ); s3BucketHasTags.run(cache, {}, callback); diff --git a/plugins/azure/apiManagement/apiInstanceManagedIdentity.js b/plugins/azure/apiManagement/apiInstanceManagedIdentity.js index 57557b018..af4031396 100644 --- a/plugins/azure/apiManagement/apiInstanceManagedIdentity.js +++ b/plugins/azure/apiManagement/apiInstanceManagedIdentity.js @@ -7,7 +7,7 @@ module.exports = { domain: 'Developer Tools', severity: 'Medium', description: 'Ensures that Azure API Management instance has managed identity enabled.', - more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.', link: 'https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-use-managed-service-identity', recommended_action: 'Modify API Management instance and add managed identity.', apis: ['apiManagementService:list'], @@ -49,4 +49,4 @@ module.exports = { callback(null, results, source); }); } -}; +}; \ No newline at end of file diff --git a/plugins/azure/appConfigurations/appConfigAccessKeyAuthDisabled.js b/plugins/azure/appConfigurations/appConfigAccessKeyAuthDisabled.js index 922bc1e66..be24ed0d9 100644 --- a/plugins/azure/appConfigurations/appConfigAccessKeyAuthDisabled.js +++ b/plugins/azure/appConfigurations/appConfigAccessKeyAuthDisabled.js @@ -7,7 +7,7 @@ module.exports = { domain: 'Developer Tools', severity: 'Low', description: 'Ensures that access key authentication is disabled for App Configuration.', - more_info: 'By default, requests can be authenticated with either Microsoft Entra credentials, or by using an access key. For enhanced security, centralized identity management, and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Active Directory (Azure AD) and disable local authentication for Azure App Configurations.', + more_info: 'By default, requests can be authenticated with either Microsoft Entra credentials, or by using an access key. For enhanced security, centralized identity management, and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Entra ID and disable local authentication for Azure App Configurations.', link: 'https://learn.microsoft.com/en-us/azure/azure-app-configuration/howto-disable-access-key-authentication', recommended_action: 'Ensure that Azure App Configurations have access key authentication disabled.', apis: ['appConfigurations:list'], @@ -49,4 +49,4 @@ module.exports = { callback(null, results, source); }); } -}; +}; \ No newline at end of file diff --git a/plugins/azure/appConfigurations/appConfigManagedIdentity.js b/plugins/azure/appConfigurations/appConfigManagedIdentity.js index 1989c003f..a76bfe0e7 100644 --- a/plugins/azure/appConfigurations/appConfigManagedIdentity.js +++ b/plugins/azure/appConfigurations/appConfigManagedIdentity.js @@ -7,7 +7,7 @@ module.exports = { domain: 'Developer Tools', severity: 'Medium', description: 'Ensures that Azure App Configurations have managed identity enabled.', - more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.', link: 'https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview-managed-identity', recommended_action: 'Modify App Configuration store and add managed identity.', apis: ['appConfigurations:list'], @@ -49,4 +49,4 @@ module.exports = { callback(null, results, source); }); } -}; +}; \ No newline at end of file diff --git a/plugins/azure/appservice/appServiceAccessRestriction.js b/plugins/azure/appservice/appServiceAccessRestriction.js index da71ba7b2..618e8145f 100644 --- a/plugins/azure/appservice/appServiceAccessRestriction.js +++ b/plugins/azure/appservice/appServiceAccessRestriction.js @@ -7,8 +7,7 @@ module.exports = { domain: 'Application Integration', severity: 'Medium', description: 'Ensure that Azure App Services have access restriction configured to control network access to your app.', - more_info: 'By setting up access restrictions, you can define a priority-ordered allow/deny list that controls network access to your app. ' + - 'The list can include IP addresses or Azure Virtual Network subnets. When there are one or more entries, an implicit deny all exists at the end of the list.', + more_info: 'By setting up access restrictions, you can define a priority-ordered allow/deny list that controls network access to your app. The list can include IP addresses or Azure Virtual Network subnets. When there are one or more entries, an implicit deny all exists at the end of the list. The most secure configuration is to disable public network access entirely. If public access is enabled, this plugin checks for explicit access restrictions with an "Any" IP address and "Deny" action rule.', recommended_action: 'Add access restriction rules under network settings for the app services', link: 'https://learn.microsoft.com/en-us/azure/app-service/app-service-ip-restrictions#set-up-azure-functions-access-restrictions', apis: ['webApps:list', 'webApps:listConfigurations'], @@ -50,20 +49,30 @@ module.exports = { 'Unable to query App Service configuration: ' + helpers.addError(webConfigs), location, webApp.id); } else { - let denyAllIp; - if (webConfigs.data[0].ipSecurityRestrictions && webConfigs.data[0].ipSecurityRestrictions.length) { - denyAllIp = webConfigs.data[0].ipSecurityRestrictions.find(ipSecurityRestriction => - ipSecurityRestriction.ipAddress && ipSecurityRestriction.ipAddress.toUpperCase() === 'ANY' && - ipSecurityRestriction.action && ipSecurityRestriction.action.toUpperCase() === 'DENY' - ); - } + const config = webConfigs.data[0]; - if (denyAllIp) { + if (config.publicNetworkAccess && config.publicNetworkAccess.toLowerCase() === 'disabled') { helpers.addResult(results, 0, 'App Service has access restriction enabled', location, webApp.id); } else { - helpers.addResult(results, 2, 'App Service does not have access restriction enabled', location, webApp.id); + let denyAllIp; + if (config.ipSecurityRestrictions && config.ipSecurityRestrictions.length) { + denyAllIp = config.ipSecurityRestrictions.find(ipSecurityRestriction => + ipSecurityRestriction.ipAddress && ipSecurityRestriction.ipAddress.toUpperCase() === 'ANY' && + ipSecurityRestriction.action && ipSecurityRestriction.action.toUpperCase() === 'DENY' + ); + } + + if (denyAllIp) { + helpers.addResult(results, 0, + 'App Service has access restriction enabled', + location, webApp.id); + } else { + helpers.addResult(results, 2, + 'App Service does not have access restriction enabled', + location, webApp.id); + } } } }); @@ -97,7 +106,7 @@ module.exports = { 'action': 'Deny', 'name': 'Deny All Access', 'ipAddress': '0.0.0.0/0', - 'description': 'Khulnasoft CSPM Auto Remediation', + 'description': 'KhulnaSoft CSPM Auto Remediation', 'priority': 2147483647 } ] @@ -130,4 +139,4 @@ module.exports = { callback('No region found'); } } -}; +}; \ No newline at end of file diff --git a/plugins/azure/appservice/appServiceAccessRestriction.spec.js b/plugins/azure/appservice/appServiceAccessRestriction.spec.js index 993770048..d6c7520d7 100644 --- a/plugins/azure/appservice/appServiceAccessRestriction.spec.js +++ b/plugins/azure/appservice/appServiceAccessRestriction.spec.js @@ -11,6 +11,7 @@ const webApps = [ const configurations = [ { 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web', + 'publicNetworkAccess': 'Enabled', 'ipSecurityRestrictions': [ { 'ipAddress': 'Any', @@ -23,6 +24,7 @@ const configurations = [ }, { 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web', + 'publicNetworkAccess': 'Enabled', 'ipSecurityRestrictions': [ { 'ipAddress': '208.130.0.0/16', @@ -39,6 +41,22 @@ const configurations = [ 'description': 'Deny all access' } ] + }, + { + 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web', + 'publicNetworkAccess': 'Disabled' + }, + { + 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web', + 'publicNetworkAccess': 'Enabled', + 'ipSecurityRestrictions': [ + { + 'ipAddress': '192.168.1.0/24', + 'action': 'Allow', + 'priority': 100, + 'name': 'Office Network' + } + ], } ]; @@ -123,7 +141,18 @@ describe('appServiceAccessRestriction', function() { }); }); - it('should give passing result if app Service has access restriction enabled', function(done) { + it('should give passing result if public network access is disabled (most secure)', function(done) { + const cache = createCache([webApps[0]], [configurations[2]]); + appServiceAccessRestriction.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('App Service has access restriction enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if app Service has explicit Any/Deny rule', function(done) { const cache = createCache([webApps[0]], [configurations[1]]); appServiceAccessRestriction.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); @@ -134,7 +163,7 @@ describe('appServiceAccessRestriction', function() { }); }); - it('should give failing result if App Service does not have access restriction enabled', function(done) { + it('should give failing result if App Service has allow all rule', function(done) { const cache = createCache([webApps[0]], [configurations[0]]); appServiceAccessRestriction.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); @@ -144,5 +173,16 @@ describe('appServiceAccessRestriction', function() { done(); }); }); + + it('should give failing result if App Service has specific IP restrictions but no Any/Deny rule', function(done) { + const cache = createCache([webApps[0]], [configurations[3]]); + appServiceAccessRestriction.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('App Service does not have access restriction enabled'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); }); }); \ No newline at end of file diff --git a/plugins/azure/appservice/authEnabled.js b/plugins/azure/appservice/authEnabled.js index 6c2cec0d3..4140da116 100644 --- a/plugins/azure/appservice/authEnabled.js +++ b/plugins/azure/appservice/authEnabled.js @@ -7,7 +7,7 @@ module.exports = { domain: 'Application Integration', severity: 'Medium', description: 'Ensures Authentication is enabled for App Services, redirecting unauthenticated users to the login page.', - more_info: 'Enabling authentication will redirect all unauthenticated requests to the login page. It also handles authentication of users with specific providers (Azure Active Directory, Facebook, Google, Microsoft Account, and Twitter).', + more_info: 'Enabling authentication will redirect all unauthenticated requests to the login page. It also handles authentication of users with specific providers (Azure Entra ID, Facebook, Google, Microsoft Account, and Twitter).', recommended_action: 'Enable App Service Authentication for all App Services.', link: 'https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization', apis: ['webApps:list', 'webApps:getAuthSettings'], @@ -134,4 +134,4 @@ module.exports = { callback('No region found'); } } -}; +}; \ No newline at end of file diff --git a/plugins/azure/appservice/automatedBackupsEnabled.spec.js b/plugins/azure/appservice/automatedBackupsEnabled.spec.js index c257c8b9c..cd3bda1d6 100644 --- a/plugins/azure/appservice/automatedBackupsEnabled.spec.js +++ b/plugins/azure/appservice/automatedBackupsEnabled.spec.js @@ -27,12 +27,12 @@ const backupConfigs = { enabled: true, storageAccountUrl: 'https://akhtarrgdiag.blob.core.windows.net/appbackup?sp=rwdl&st=2022-03-16T07:51:37Z&se=2295-12-29T08:51:37Z&sv=2020-08-04&sr=c&sig=FeC0hGUrqJb6b%2Bh5qbIif84725sMjeqyNUzWa4tL3L4%3D', backupSchedule: { - frequencyInterval: 7, - frequencyUnit: 'Day', - keepAtLeastOneBackup: true, - retentionPeriodInDays: 7, - startTime: '2022-03-16T07:51:38.699', - lastExecutionTime: '2022-03-16T07:53:38.4131659' + frequencyInterval: 7, + frequencyUnit: 'Day', + keepAtLeastOneBackup: true, + retentionPeriodInDays: 7, + startTime: '2022-03-16T07:51:38.699', + lastExecutionTime: '2022-03-16T07:53:38.4131659' }, databases: [], mySqlDumpParams: null @@ -128,7 +128,7 @@ describe('automatedBackupsEnabled', function() { automatedBackupsEnabled.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(2); - expect(results[0].message).to.include('Backups are not configured for WebApp'); + expect(results[0].message).to.include('Custom Backups are not configured for WebApp'); expect(results[0].region).to.equal('eastus'); done(); }); @@ -139,10 +139,10 @@ describe('automatedBackupsEnabled', function() { automatedBackupsEnabled.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(0); - expect(results[0].message).to.include('Backups are configured for WebApp'); + expect(results[0].message).to.include('Custom Backups are configured for WebApp'); expect(results[0].region).to.equal('eastus'); done(); }); }); }); -}); +}); \ No newline at end of file diff --git a/plugins/azure/appservice/functionPrivilegeAnalysis.js b/plugins/azure/appservice/functionPrivilegeAnalysis.js new file mode 100644 index 000000000..3b8585eb5 --- /dev/null +++ b/plugins/azure/appservice/functionPrivilegeAnalysis.js @@ -0,0 +1,24 @@ +module.exports = { + title: 'Privilege Analysis', + category: 'App Service', + domain: 'Web Apps', + severity: 'Info', + description: 'Ensures that no Azure Functions in your environment have excessive permissions.', + more_info: 'Azure Functions that use managed identities or service principals with excessive Azure AD permissions may pose security risks. It is a best practice to assign only the necessary permissions to the identities attached to functions.', + link: 'https://docs.microsoft.com/en-us/azure/app-service/overview-managed-identity', + recommended_action: 'Review and restrict the Azure AD roles associated with managed identities used by Azure Functions to follow the principle of least privilege.', + apis: [''], + realtime_triggers: [ + 'Microsoft.Web/sites/write', + 'Microsoft.Web/sites/delete', + 'Microsoft.Web/sites/functions/write', + 'Microsoft.Web/sites/functions/delete', + ], + + run: function(cache, settings, callback) { + var results = []; + var source = {}; + + callback(null, results, source); + }, +}; \ No newline at end of file diff --git a/plugins/azure/appservice/privateEndpointsEnabled.js b/plugins/azure/appservice/privateEndpointsEnabled.js index 454216400..9e56d8438 100644 --- a/plugins/azure/appservice/privateEndpointsEnabled.js +++ b/plugins/azure/appservice/privateEndpointsEnabled.js @@ -6,11 +6,11 @@ module.exports = { category: 'App Service', domain: 'Application Integration', severity: 'Medium', - description: 'Ensures that Web Apps are accessible only through private endpoints.', - more_info: 'Enabling private endpoints for Azure App Service enhances security by allowing access exclusively through a private network, minimizing the risk of public internet exposure and protecting against external attacks.', - recommended_action: 'Ensure that Private Endpoints are configured properly and Public Network Access is disabled for Web Apps.', + description: 'Ensures that Web Apps and Function Apps are accessible only through private endpoints.', + more_info: 'Enabling private endpoints for Azure App Service and Function Apps enhances security by allowing access exclusively through a private network, minimizing the risk of public internet exposure and protecting against external attacks.', + recommended_action: 'Ensure that Private Endpoints are configured properly and Public Network Access is disabled for Web Apps and Function Apps.', link: 'https://learn.microsoft.com/en-us/azure/app-service/overview-private-endpoint', - apis: ['webApps:list'], + apis: ['webApps:list', 'webApps:getWebAppDetails'], realtime_triggers: ['microsoftweb:sites:write', 'microsoftweb:sites:privateendpointconnectionproxies:write', 'microsoftweb:sites:privateendpointconnectionproxies:delete', 'microsoftweb:sites:delete'], run: function(cache, settings, callback) { @@ -35,18 +35,41 @@ module.exports = { } webApps.data.forEach(function(webApp) { - if (webApp && webApp.kind && webApp.kind === 'functionapp') { - helpers.addResult(results, 0, 'Private Endpoints can not be configured for function apps', location, webApp.id); - } else if (webApp && webApp.privateLinkIdentifiers) { - helpers.addResult(results, 0, 'App Service has Private Endpoints configured', location, webApp.id); + if (!webApp || !webApp.id) return; + + const webAppDetails = helpers.addSource(cache, source, + ['webApps', 'getWebAppDetails', location, webApp.id]); + + let hasPrivateEndpoints = false; + + if (webAppDetails && !webAppDetails.err && webAppDetails.data && webAppDetails.data.privateEndpointConnections) { + if (Array.isArray(webAppDetails.data.privateEndpointConnections) && webAppDetails.data.privateEndpointConnections.length > 0) { + hasPrivateEndpoints = true; + } + } + + if (!hasPrivateEndpoints && webApp.privateEndpointConnections && webApp.privateEndpointConnections.length > 0) { + hasPrivateEndpoints = true; + } + + if (hasPrivateEndpoints) { + if (webApp.kind && webApp.kind.toLowerCase().includes('functionapp')) { + helpers.addResult(results, 0, 'Function App has Private Endpoints configured', location, webApp.id); + } else { + helpers.addResult(results, 0, 'App Service has Private Endpoints configured', location, webApp.id); + } } else { - helpers.addResult(results, 2, 'App Service does not have Private Endpoints configured', location, webApp.id); + // No private endpoints configured + if (webApp.kind && webApp.kind.toLowerCase().includes('functionapp')) { + helpers.addResult(results, 2, 'Function App does not have Private Endpoints configured', location, webApp.id); + } else { + helpers.addResult(results, 2, 'App Service does not have Private Endpoints configured', location, webApp.id); + } } }); rcb(); }, function() { - // Global checking goes here callback(null, results, source); }); } -}; +}; \ No newline at end of file diff --git a/plugins/azure/appservice/privateEndpointsEnabled.spec.js b/plugins/azure/appservice/privateEndpointsEnabled.spec.js index 2482ca00e..efce9f52f 100644 --- a/plugins/azure/appservice/privateEndpointsEnabled.spec.js +++ b/plugins/azure/appservice/privateEndpointsEnabled.spec.js @@ -5,25 +5,67 @@ const webApps = [ { 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1', 'name': 'app1', - 'privateLinkIdentifiers': '123456' + 'kind': 'app', + 'privateEndpointConnections': [ + { + 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/privateEndpointConnections/test-endpoint', + 'name': 'test-endpoint' + } + ] }, { - 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1', - 'name': 'app1', - 'privateLinkIdentifiers': '' + 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app2', + 'name': 'app2', + 'kind': 'app', + 'privateEndpointConnections': [] + }, + { + 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/func1', + 'name': 'func1', + 'kind': 'functionapp', + 'privateEndpointConnections': [ + { + 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/func1/privateEndpointConnections/func-endpoint', + 'name': 'func-endpoint' + } + ] + }, + { + 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/func2', + 'name': 'func2', + 'kind': 'functionapp', + 'privateEndpointConnections': [] } ]; -const createCache = (webApps) => { - return { +const createCache = (webApps, privateEndpointConnections) => { + let cache = { webApps: { list: { - 'eastus':{ + 'eastus': { data: webApps } } } }; + + if (privateEndpointConnections && webApps) { + cache.webApps.getWebAppDetails = { + 'eastus': {} + }; + webApps.forEach((webApp, index) => { + if (webApp && webApp.id) { + cache.webApps.getWebAppDetails['eastus'][webApp.id] = { + data: { + ...webApp, + privateEndpointConnections: privateEndpointConnections[index] || [] + } + }; + } + }); + } + + return cache; }; const createErrorCache = () => { @@ -61,7 +103,7 @@ describe('privateEndpointsEnabled', function() { }); it('should give passing result if app service has Private Endpoints configured', function(done) { - const cache = createCache([webApps[0]]); + const cache = createCache([webApps[0]], [[{id: 'endpoint1', name: 'test-endpoint'}]]); privateEndpointsEnabled.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(0); @@ -71,8 +113,8 @@ describe('privateEndpointsEnabled', function() { }); }); - it('should give failing result if app service app service does not have Private Endpoints configured', function(done) { - const cache = createCache([webApps[1]]); + it('should give failing result if app service does not have Private Endpoints configured', function(done) { + const cache = createCache([webApps[1]], [[]]); privateEndpointsEnabled.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(2); @@ -81,5 +123,27 @@ describe('privateEndpointsEnabled', function() { done(); }); }); + + it('should give passing result if function app has Private Endpoints configured', function(done) { + const cache = createCache([webApps[2]], [[{id: 'func-endpoint', name: 'func-test-endpoint'}]]); + privateEndpointsEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('Function App has Private Endpoints configured'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give failing result if function app does not have Private Endpoints configured', function(done) { + const cache = createCache([webApps[3]], [[]]); + privateEndpointsEnabled.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(2); + expect(results[0].message).to.include('Function App does not have Private Endpoints configured'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); }); }); \ No newline at end of file diff --git a/plugins/azure/appservice/scmSiteAccessRestriction.js b/plugins/azure/appservice/scmSiteAccessRestriction.js index 5539f199f..56d2a7cb4 100644 --- a/plugins/azure/appservice/scmSiteAccessRestriction.js +++ b/plugins/azure/appservice/scmSiteAccessRestriction.js @@ -45,20 +45,28 @@ module.exports = { 'Unable to query App Service configuration: ' + helpers.addError(webConfigs), location, webApp.id); } else { - let denyAllIp; - if (webConfigs.data[0].scmIpSecurityRestrictions && webConfigs.data[0].scmIpSecurityRestrictions.length) { - denyAllIp = webConfigs.data[0].scmIpSecurityRestrictions.find(ipSecurityRestriction => - ipSecurityRestriction.ipAddress && ipSecurityRestriction.ipAddress.toUpperCase() === 'ANY' && - ipSecurityRestriction.action && ipSecurityRestriction.action.toUpperCase() === 'DENY' - ); - } + const config = webConfigs.data[0]; - if (denyAllIp) { + if (config.publicNetworkAccess && config.publicNetworkAccess.toLowerCase() === 'disabled') { helpers.addResult(results, 0, 'App Service has access restriction enabled for scm site', location, webApp.id); } else { - helpers.addResult(results, 2, 'App Service does not have access restriction enabled for scm site', location, webApp.id); + let denyAllIp; + if (config.scmIpSecurityRestrictions && config.scmIpSecurityRestrictions.length) { + denyAllIp = config.scmIpSecurityRestrictions.find(ipSecurityRestriction => + ipSecurityRestriction.ipAddress && ipSecurityRestriction.ipAddress.toUpperCase() === 'ANY' && + ipSecurityRestriction.action && ipSecurityRestriction.action.toUpperCase() === 'DENY' + ); + } + + if (denyAllIp) { + helpers.addResult(results, 0, + 'App Service has access restriction enabled for scm site', + location, webApp.id); + } else { + helpers.addResult(results, 2, 'App Service does not have access restriction enabled for scm site', location, webApp.id); + } } } }); diff --git a/plugins/azure/appservice/scmSiteAccessRestriction.spec.js b/plugins/azure/appservice/scmSiteAccessRestriction.spec.js index a1e128da4..8d8847f10 100644 --- a/plugins/azure/appservice/scmSiteAccessRestriction.spec.js +++ b/plugins/azure/appservice/scmSiteAccessRestriction.spec.js @@ -39,6 +39,24 @@ const configurations = [ 'description': 'Deny all access' } ] + }, + { + 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web', + 'publicNetworkAccess': 'Disabled', + 'scmIpSecurityRestrictions': [] + }, + { + 'id': '/subscriptions/123/resourceGroups/khulnasoft-resource-group/providers/Microsoft.Web/sites/app1/config/web', + 'publicNetworkAccess': 'Disabled', + 'scmIpSecurityRestrictions': [ + { + 'ipAddress': 'Any', + 'action': 'Allow', + 'priority': 1, + 'name': 'Allow all', + 'description': 'Allow all access' + } + ] } ]; @@ -144,5 +162,27 @@ describe('scmSiteAccessRestriction', function() { done(); }); }); + + it('should give passing result if App Service has public network access disabled with no IP restrictions', function(done) { + const cache = createCache([webApps[0]], [configurations[2]]); + scmSiteAccessRestriction.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('App Service has access restriction enabled for scm site'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); + + it('should give passing result if App Service has public network access disabled even with allow all IP restrictions', function(done) { + const cache = createCache([webApps[0]], [configurations[3]]); + scmSiteAccessRestriction.run(cache, {}, (err, results) => { + expect(results.length).to.equal(1); + expect(results[0].status).to.equal(0); + expect(results[0].message).to.include('App Service has access restriction enabled for scm site'); + expect(results[0].region).to.equal('eastus'); + done(); + }); + }); }); }); \ No newline at end of file diff --git a/plugins/azure/appservice/webAppsADEnabled.js b/plugins/azure/appservice/webAppsADEnabled.js index 6c4caf0da..71625e3b0 100644 --- a/plugins/azure/appservice/webAppsADEnabled.js +++ b/plugins/azure/appservice/webAppsADEnabled.js @@ -2,13 +2,13 @@ var async = require('async'); var helpers = require('../../../helpers/azure'); module.exports = { - title: 'Web Apps Active Directory Enabled', + title: 'Web Apps Entra ID Enabled', category: 'App Service', domain: 'Application Integration', severity: 'Medium', - description: 'Ensures that Azure Web Apps have registration with Azure Active Directory.', - more_info: 'Registration with Azure Active Directory (AAD) enables App Service web applications to connect to other Azure cloud services securely without the need of access credentials such as user names and passwords.', - recommended_action: 'Enable registration with Azure Active Directory for Azure Web Apps.', + description: 'Ensures that Azure Web Apps have registration with Azure Entra ID.', + more_info: 'Registration with Azure Entra ID enables App Service web applications to connect to other Azure cloud services securely without the need of access credentials such as user names and passwords.', + recommended_action: 'Enable registration with Azure Entra ID for Azure Web Apps.', link: 'https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal%2Chttp#add-a-system-assigned-identity', apis: ['webApps:list'], realtime_triggers: ['microsoftweb:sites:write','microsoftweb:sites:delete'], @@ -36,9 +36,9 @@ module.exports = { for (let app of webApps.data) { if (app.identity && app.identity.principalId) { - helpers.addResult(results, 0, 'Registration with Azure Active Directory is enabled for the Web App', location, app.id); + helpers.addResult(results, 0, 'Registration with Azure Entra ID is enabled for the Web App', location, app.id); } else { - helpers.addResult(results, 2, 'Registration with Azure Active Directory is disabled for the Web App', location, app.id); + helpers.addResult(results, 2, 'Registration with Azure Entra ID is disabled for the Web App', location, app.id); } } @@ -47,4 +47,4 @@ module.exports = { callback(null, results, source); }); } -}; +}; \ No newline at end of file diff --git a/plugins/azure/appservice/webAppsADEnabled.spec.js b/plugins/azure/appservice/webAppsADEnabled.spec.js index 06b8be84f..530e55d0e 100644 --- a/plugins/azure/appservice/webAppsADEnabled.spec.js +++ b/plugins/azure/appservice/webAppsADEnabled.spec.js @@ -72,26 +72,26 @@ describe('webAppsADEnabled', function() { }); }); - it('should give passing result if Registration with Azure Active Directory is enabled', function(done) { + it('should give passing result if Registration with Azure Entra ID is enabled', function(done) { const cache = createCache([webApps[1]]); webAppsADEnabled.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(0); - expect(results[0].message).to.include('Registration with Azure Active Directory is enabled for the Web App'); + expect(results[0].message).to.include('Registration with Azure Entra ID is enabled for the Web App'); expect(results[0].region).to.equal('eastus'); done(); }); }); - it('should give failing result if Registration with Azure Active Directory is disabled', function(done) { + it('should give failing result if Registration with Azure Entra ID is disabled', function(done) { const cache = createCache([webApps[0]]); webAppsADEnabled.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(2); - expect(results[0].message).to.include('Registration with Azure Active Directory is disabled for the Web App'); + expect(results[0].message).to.include('Registration with Azure Entra ID is disabled for the Web App'); expect(results[0].region).to.equal('eastus'); done(); }); }); }); -}); +}); \ No newline at end of file diff --git a/plugins/azure/automationAccounts/automationAcctManagedIdentity.js b/plugins/azure/automationAccounts/automationAcctManagedIdentity.js index bf3fbe446..b946d8c4e 100644 --- a/plugins/azure/automationAccounts/automationAcctManagedIdentity.js +++ b/plugins/azure/automationAccounts/automationAcctManagedIdentity.js @@ -7,7 +7,7 @@ module.exports = { domain: 'Management and Governance', severity: 'Medium', description: 'Ensure that Azure Automation accounts have managed identity enabled.', - more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.', recommended_action: 'Modify automation account and enable managed identity.', link: 'https://learn.microsoft.com/en-us/azure/automation/quickstarts/enable-managed-identity', apis: ['automationAccounts:list'], @@ -50,5 +50,4 @@ module.exports = { callback(null, results, source); }); } -}; - +}; \ No newline at end of file diff --git a/plugins/azure/batchAccounts/batchAccountsAADEnabled.js b/plugins/azure/batchAccounts/batchAccountsAADEnabled.js index cf9cabc3a..fc2839ecd 100644 --- a/plugins/azure/batchAccounts/batchAccountsAADEnabled.js +++ b/plugins/azure/batchAccounts/batchAccountsAADEnabled.js @@ -2,13 +2,13 @@ var async = require('async'); var helpers = require('../../../helpers/azure/'); module.exports = { - title: 'Batch Account AAD Auth Enabled', + title: 'Batch Account Entra ID Auth Enabled', category: 'Batch', domain: 'Compute', severity: 'Medium', - description: 'Ensures that Batch account has Azure Active Directory (AAD) authentication mode enabled.', - more_info: 'Enabling Azure Active Directory (AAD) authentication for Batch account ensures enhanced security by restricting the service API authentication to Microsoft Entra ID that prevents access through less secure shared key methods, thereby safeguarding batch resources from unauthorized access.', - recommended_action: 'Enable Active Directory authentication mode for all Batch accounts.', + description: 'Ensures that Batch account has Azure Entra ID authentication mode enabled.', + more_info: 'Enabling Azure Entra ID authentication for Batch account ensures enhanced security by restricting the service API authentication to Microsoft Entra ID that prevents access through less secure shared key methods, thereby safeguarding batch resources from unauthorized access.', + recommended_action: 'Enable Entra ID authentication mode for all Batch accounts.', link: 'https://learn.microsoft.com/en-us/azure/batch/batch-aad-auth', apis: ['batchAccounts:list'], realtime_triggers: ['microsoftbatch:batchaccounts:write', 'microsoftbatch:batchaccounts:delete'], @@ -42,9 +42,9 @@ module.exports = { batchAccount.allowedAuthenticationModes.some(mode => mode.toUpperCase() === 'AAD') : false; if (found) { - helpers.addResult(results, 0, 'Batch account has Active Directory authentication enabled', location, batchAccount.id); + helpers.addResult(results, 0, 'Batch account has Entra ID authentication enabled', location, batchAccount.id); } else { - helpers.addResult(results, 2, 'Batch account does not have Active Directory authentication enabled', location, batchAccount.id); + helpers.addResult(results, 2, 'Batch account does not have Entra ID authentication enabled', location, batchAccount.id); } } diff --git a/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js b/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js index cd106bc09..508c96d69 100644 --- a/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js +++ b/plugins/azure/batchAccounts/batchAccountsAADEnabled.spec.js @@ -69,23 +69,23 @@ describe('batchAccountsAADEnabled', function () { }); }); - it('should give passing result if Batch account is configured with AAD Authentication', function (done) { + it('should give passing result if Batch account is configured with Entra ID Authentication', function (done) { const cache = createCache([batchAccounts[0]]); batchAccountsAADEnabled.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(0); - expect(results[0].message).to.include('Batch account has Active Directory authentication enabled'); + expect(results[0].message).to.include('Batch account has Entra ID authentication enabled'); expect(results[0].region).to.equal('eastus'); done(); }); }); - it('should give failing result if Batch account is not configured with AAD Authentication', function (done) { + it('should give failing result if Batch account is not configured with Entra ID Authentication', function (done) { const cache = createCache([batchAccounts[1]]); batchAccountsAADEnabled.run(cache, {}, (err, results) => { expect(results.length).to.equal(1); expect(results[0].status).to.equal(2); - expect(results[0].message).to.include('Batch account does not have Active Directory authentication enabled'); + expect(results[0].message).to.include('Batch account does not have Entra ID authentication enabled'); expect(results[0].region).to.equal('eastus'); done(); }); diff --git a/plugins/azure/batchAccounts/batchAccountsManagedIdentity.js b/plugins/azure/batchAccounts/batchAccountsManagedIdentity.js index 767c81eb9..4b0821e75 100644 --- a/plugins/azure/batchAccounts/batchAccountsManagedIdentity.js +++ b/plugins/azure/batchAccounts/batchAccountsManagedIdentity.js @@ -7,7 +7,7 @@ module.exports = { domain: 'Compute', severity: 'Medium', description: 'Ensures that Batch accounts have managed identity enabled.', - more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure and using it to obtain Azure Entra Id tokens.', recommended_action: 'Modify Batch Account and enable managed identity.', link: 'https://learn.microsoft.com/en-us/troubleshoot/azure/hpc/batch/use-managed-identities-azure-batch-account-pool', apis: ['batchAccounts:list'], diff --git a/plugins/azure/containerapps/containerAppManagedIdentity.js b/plugins/azure/containerapps/containerAppManagedIdentity.js index 6fb376db7..978ecbd01 100644 --- a/plugins/azure/containerapps/containerAppManagedIdentity.js +++ b/plugins/azure/containerapps/containerAppManagedIdentity.js @@ -7,7 +7,7 @@ module.exports = { domain: 'Containers', severity: 'Medium', description: 'Ensure that Azure Container Apps has managed identity enabled.', - more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.', recommended_action: 'Modify Container apps and add managed identity.', link: 'https://learn.microsoft.com/en-us/azure/container-apps/managed-identity', apis: ['containerApps:list'], diff --git a/plugins/azure/containerregistry/acrManagedIdentityEnabled.js b/plugins/azure/containerregistry/acrManagedIdentityEnabled.js index a5396cec2..6c6012aa6 100644 --- a/plugins/azure/containerregistry/acrManagedIdentityEnabled.js +++ b/plugins/azure/containerregistry/acrManagedIdentityEnabled.js @@ -7,7 +7,7 @@ module.exports = { domain: 'Containers', severity: 'Medium', description: 'Ensure that Azure container registries have managed identity enabled.', - more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Active Directory (Azure AD) tokens.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.', recommended_action: 'Modify container registry and enable managed identity.', link: 'https://learn.microsoft.com/en-us/azure/container-registry/container-registry-authentication-managed-identity?tabs=azure-cli', apis: ['registries:list'], @@ -50,4 +50,4 @@ module.exports = { callback(null, results, source); }); } -}; +}; \ No newline at end of file diff --git a/plugins/azure/cosmosdb/cosmosdbLocalAuth.js b/plugins/azure/cosmosdb/cosmosdbLocalAuth.js index 8efe7ca7b..7090696b5 100644 --- a/plugins/azure/cosmosdb/cosmosdbLocalAuth.js +++ b/plugins/azure/cosmosdb/cosmosdbLocalAuth.js @@ -7,7 +7,7 @@ module.exports = { domain: 'Databases', severity: 'Low', description: 'Ensures that local authentication is disabled for Cosmos DB accounts.', - more_info: 'For enhanced security, centralized identity management and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Active Directory (Azure AD) and disable local authentication for Azure Cosmos DB accounts.', + more_info: 'For enhanced security, centralized identity management and seamless integration with Azure\'s authentication and authorization services, it is recommended to rely on Azure Entra ID and disable local authentication for Azure Cosmos DB accounts.', recommended_action: 'Ensure that Cosmos DB accounts have local authentication disabled.', link: 'https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#disable-local-auth', apis: ['databaseAccounts:list'], diff --git a/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js b/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js index b1f8054a2..6f81c11c8 100644 --- a/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js +++ b/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js @@ -1,137 +1,56 @@ -var expect = require('chai').expect; -var cosmosdbManagedIdentity = require('./cosmosdbManagedIdentity'); - -const databaseAccounts = [ - { - "id": "/subscriptions/123/resourceGroups/tets-rg/providers/Microsoft.DocumentDB/databaseAccounts/khulnasoft-cosmos", - "name": "khulnasoft-cosmos", - "location": "East US", - "tags": {"key": "value"}, - "type": "Microsoft.DocumentDB/databaseAccounts", - "kind": "GlobalDocumentDB", - "provisioningState": "Succeeded", - "identity": { - "principalId":"e8c02afc-8fb0-43eb-985f-5bb60a87e7aa", - "type":"systemassigned" - }, - "documentEndpoint": "https://khulnasoft-cosmos.documents.azure.com:443/", - "publicNetworkAccess": "Enabled", - "enableAutomaticFailover": true, - "enableMultipleWriteLocations": false, - "enablePartitionKeyMonitor": false, - "isVirtualNetworkFilterEnabled": true, - "EnabledApiTypes": "Sql", - "disableKeyBasedMetadataWriteAccess": false, - "enableAnalyticalStorage": false, - "instanceId": "5f3e6edc-33c6-4a47-81aa-108af12d4fba", - "createMode": "Default", - "databaseAccountOfferType": "Standard", - }, - { - "id": "/subscriptions/123/resourceGroups/tets-rg/providers/Microsoft.DocumentDB/databaseAccounts/khulnasoft-cosmos", - "name": "khulnasoft-cosmos", - "location": "East US", - "tags": {}, - "type": "Microsoft.DocumentDB/databaseAccounts", - "kind": "GlobalDocumentDB", - "provisioningState": "Succeeded", - "documentEndpoint": "https://khulnasoft-cosmos.documents.azure.com:443/", - "publicNetworkAccess": "Enabled", - "identity": { - "type":"None" - }, - "enableAutomaticFailover": false, - "enableMultipleWriteLocations": false, - "enablePartitionKeyMonitor": false, - "isVirtualNetworkFilterEnabled": false, - "virtualNetworkRules": [], - "EnabledApiTypes": "Cassandra", - "disableKeyBasedMetadataWriteAccess": false, - "enableAnalyticalStorage": false, - "instanceId": "5f3e6edc-33c6-4a47-81aa-108af12d4fba", - "createMode": "Default", - "databaseAccountOfferType": "Standard" - } -]; - -const createCache = (accounts, accountsErr) => { - return { - databaseAccounts: { - list: { - 'eastus': { - err: accountsErr, - data: accounts - } +const async = require('async'); +const helpers = require('../../../helpers/azure'); + +module.exports = { + title: 'Cosmos DB Managed Identity', + category: 'Cosmos DB', + domain: 'Databases', + severity: 'Medium', + description: 'Ensures that Azure Cosmos DB accounts have managed identity enabled.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.', + link: 'https://learn.microsoft.com/en-us/azure/cosmos-db/managed-identity-based-authentication', + recommended_action: 'Enable system or user-assigned identities for all Azure Cosmos DB accounts.', + apis: ['databaseAccounts:list'], + realtime_triggers: ['microsoftdocumentdb:databaseaccounts:write','microsoftdocumentdb:databaseaccounts:delete'], + + run: function(cache, settings, callback) { + const results = []; + const source = {}; + const locations = helpers.locations(settings.govcloud); + + async.each(locations.databaseAccounts, function(location, rcb) { + var databaseAccounts = helpers.addSource(cache, source, + ['databaseAccounts', 'list', location]); + + if (!databaseAccounts) return rcb(); + + if (databaseAccounts.err || !databaseAccounts.data) { + helpers.addResult(results, 3, + 'Unable to query for Cosmos DB accounts: ' + helpers.addError(databaseAccounts), location); + return rcb(); } - } - } -}; - -describe('cosmosdbManagedIdentity', function() { - describe('run', function() { - it('should give passing result if no Cosmos DB accounts found', function(done) { - const callback = (err, results) => { - expect(results.length).to.equal(1); - expect(results[0].status).to.equal(0); - expect(results[0].message).to.include('No Cosmos DB accounts found'); - expect(results[0].region).to.equal('eastus'); - done() - }; - - const cache = createCache( - [] - ); - - cosmosdbManagedIdentity.run(cache, {}, callback); - }); - - it('should give passing result if Cosmos db has managed identity', function(done) { - const callback = (err, results) => { - expect(results.length).to.equal(1); - expect(results[0].status).to.equal(0); - expect(results[0].message).to.include('Cosmos DB account has managed identity enabled'); - expect(results[0].region).to.equal('eastus'); - done() - }; - const cache = createCache( - [databaseAccounts[0]] - ); - - cosmosdbManagedIdentity.run(cache, {}, callback); - }); - - it('should give failing result if Azure Cosmos db does not have managed identity', function(done) { - const callback = (err, results) => { - expect(results.length).to.equal(1); - expect(results[0].status).to.equal(2); - expect(results[0].message).to.include('Cosmos DB account does not have managed identity enabled'); - expect(results[0].region).to.equal('eastus'); - done() - }; - - const cache = createCache( - [databaseAccounts[1]], - ); - - cosmosdbManagedIdentity.run(cache, {}, callback); - }); + if (!databaseAccounts.data.length) { + helpers.addResult(results, 0, 'No Cosmos DB accounts found', location); + return rcb(); + } - it('should give unknown result if unable to query for Cosmos DB accounts', function(done) { - const callback = (err, results) => { - expect(results.length).to.equal(1); - expect(results[0].status).to.equal(3); - expect(results[0].message).to.include('Unable to query for Cosmos DB accounts'); - expect(results[0].region).to.equal('eastus'); - done() - }; + databaseAccounts.data.forEach(account => { + if (!account.id) return; - const cache = createCache( - [], - { message: 'Unable to query Cosmos DB accounts'} - ); + if (account.identity && account.identity.type && + (account.identity.type.toLowerCase() === 'systemassigned' || account.identity.type.toLowerCase() === 'userassigned')) { + helpers.addResult(results, 0, + 'Cosmos DB account has managed identity enabled', location, account.id); + } else { + helpers.addResult(results, 2, + 'Cosmos DB account does not have managed identity enabled', location, account.id); + } + }); - cosmosdbManagedIdentity.run(cache, {}, callback); + rcb(); + }, function() { + callback(null, results, source); }); - }) -}); + } +}; \ No newline at end of file diff --git a/postprocess/output.js b/postprocess/output.js index fdf30b4a8..8242b4903 100644 --- a/postprocess/output.js +++ b/postprocess/output.js @@ -293,23 +293,115 @@ module.exports = { }, /** - * Creates an output handler that writes collection in the JSON format. - * @param {fs.WriteSteam} stream The stream to write to or an object that + * Creates an output handler that writes collection in a format suitable for SaaS integration. + * @param {fs.WriteStream} stream The stream to write to or an object that * obeys the writeable stream contract. + * @param {Object} settings Configuration settings including scan metadata */ createCollection: function(stream, settings) { - var results = {}; + var scanData = { + // Standard metadata + metadata: { + version: '1.0', + scan_timestamp: new Date().toISOString(), + scan_id: settings.scanId || `local_${Date.now()}`, + account_id: settings.accountId || 'local', + cloud_provider: settings.cloud || 'unknown', + // Add any additional metadata from settings + ...(settings.metadata || {}) + }, + // Collection data will be stored here + resources: {}, + // Summary information + summary: { + total_resources: 0, + scan_duration: 0, + status: 'completed' + } + }; + return { stream: stream, write: function(collection) { - results = collection; + // Process the collection data + if (collection && typeof collection === 'object') { + // Add resources to the scan data + scanData.resources = collection; + + // Update summary information + if (collection.resources) { + scanData.summary.total_resources = collection.resources.length || 0; + } + } }, close: function() { - this.stream.write(JSON.stringify(results, null, 2)); + // Calculate scan duration if start time is available + if (settings.startTime) { + const endTime = new Date(); + scanData.summary.scan_duration = endTime - new Date(settings.startTime); + } + + // Write the formatted output + const output = JSON.stringify(scanData, null, 2); + this.stream.write(output); this.stream.end(); - log(`INFO: Collection file written to ${settings.collection}`, settings); + + log(`INFO: Collection data written to ${settings.collection}`, settings); + + // If SaaS endpoint is configured, send the data + if (settings.saas && settings.saas.endpoint) { + this._sendToSaaS(scanData, settings); + } + }, + + /** + * Sends the scan data to the SaaS endpoint + * @private + */ + _sendToSaaS: function(data, settings) { + const https = require('https'); + const url = require('url'); + + const saasUrl = url.parse(settings.saas.endpoint); + const postData = JSON.stringify(data); + + const options = { + hostname: saasUrl.hostname, + port: saasUrl.port || 443, + path: saasUrl.path, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + 'Authorization': settings.saas.apiKey ? `Bearer ${settings.saas.apiKey}` : '' + } + }; + + const req = https.request(options, (res) => { + let responseData = ''; + + res.on('data', (chunk) => { + responseData += chunk; + }); + + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + log('INFO: Successfully sent data to SaaS platform', settings); + } else { + log(`WARN: Failed to send data to SaaS platform: ${res.statusCode} - ${responseData}`, settings); + } + }); + }); + + req.on('error', (e) => { + log(`ERROR: Failed to send data to SaaS platform: ${e.message}`, settings); + }); + + // Write data to request body + req.write(postData); + req.end(); } }; }, From cdb134ab836e9bce8500853c9894514fcd41a61b Mon Sep 17 00:00:00 2001 From: envrs Date: Thu, 4 Dec 2025 01:45:00 +0600 Subject: [PATCH 03/14] Delete .env.example Signed-off-by: envrs --- .env.example | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 30aa8515c..000000000 --- a/.env.example +++ /dev/null @@ -1,27 +0,0 @@ -# Cloud Provider Credentials -AWS_ACCESS_KEY_ID=your_aws_access_key -AWS_SECRET_ACCESS_KEY=your_aws_secret_key -AWS_REGION=us-east-1 - -# Azure Credentials -AZURE_TENANT_ID=your_tenant_id -AZURE_CLIENT_ID=your_client_id -AZURE_CLIENT_SECRET=your_client_secret -AZURE_SUBSCRIPTION_ID=your_subscription_id - -# Google Cloud Credentials -GOOGLE_APPLICATION_CREDENTIALS=path/to/credentials.json - -# GitHub Credentials -GITHUB_TOKEN=your_github_token -GITHUB_ORG=your_github_org - -# Application Settings -NODE_ENV=development -LOG_LEVEL=info - -# Security Settings -ENCRYPTION_KEY=your_encryption_key_here - -# API Rate Limiting -MAX_REQUESTS_PER_MINUTE=60 From 891b327e8eee431b176386b0c11739e31eb4ec81 Mon Sep 17 00:00:00 2001 From: envrs Date: Thu, 4 Dec 2025 01:45:20 +0600 Subject: [PATCH 04/14] Delete .eslintrc.json Signed-off-by: envrs --- .eslintrc.json | 112 ------------------------------------------------- 1 file changed, 112 deletions(-) delete mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index a15f32ad8..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "root": true, - "env": { - "node": true, - "es2021": true, - "mocha": true - }, - "extends": [ - "eslint:recommended", - "airbnb-base", - "plugin:security/recommended", - "plugin:jsdoc/recommended", - "plugin:prettier/recommended" - ], - "parser": "@babel/eslint-parser", - "parserOptions": { - "ecmaVersion": 2021, - "sourceType": "module", - "requireConfigFile": false, - "ecmaFeatures": { - "jsx": true - } - }, - "plugins": ["security", "jsdoc", "prettier"], - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".json"], - "moduleDirectory": ["node_modules", "src/"] - } - }, - "jsdoc": { - "tagNamePreference": { - "returns": "return" - } - } - }, - "ignorePatterns": [ - "node_modules/", - "coverage/", - "dist/", - "build/", - "*.test.js", - "*.spec.js", - "*.config.js" - ], - "rules": { - "prettier/prettier": "error", - "no-console": ["warn", { "allow": ["warn", "error", "info"] }], - "no-underscore-dangle": ["error", { "allowAfterThis": true }], - "class-methods-use-this": "off", - "consistent-return": "off", - "no-param-reassign": ["error", { "props": false }], - "import/prefer-default-export": "off", - "import/extensions": ["error", "ignorePackages"], - "no-await-in-loop": "warn", - "no-restricted-syntax": [ - "error", - { - "selector": "ForInStatement", - "message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array." - }, - { - "selector": "LabeledStatement", - "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand." - }, - { - "selector": "WithStatement", - "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize." - } - ], - "security/detect-object-injection": "warn", - "security/detect-non-literal-fs-filename": "warn", - "security/detect-unsafe-regex": "warn", - "security/detect-buffer-noassert": "warn", - "security/detect-child-process": "warn", - "security/detect-disable-mustache-escape": "warn", - "security/detect-eval-with-expression": "warn", - "security/detect-new-buffer": "warn", - "security/detect-no-csrf-before-method-override": "warn", - "security/detect-non-literal-require": "warn", - "security/detect-pseudoRandomBytes": "warn", - "security/detect-possible-timing-attacks": "warn", - "security/detect-bidi-characters": "warn", - "jsdoc/require-param-description": "warn", - "jsdoc/require-returns-description": "warn", - "jsdoc/require-jsdoc": [ - "warn", - { - "require": { - "FunctionDeclaration": true, - "MethodDefinition": true, - "ClassDeclaration": true, - "ArrowFunctionExpression": true - } - } - ] - }, - "overrides": [ - { - "files": ["**/*.spec.js", "**/*.test.js"], - "env": { - "mocha": true - }, - "rules": { - "no-unused-expressions": "off", - "no-shadow": "off", - "no-param-reassign": "off" - } - } - ] -} \ No newline at end of file From 646e02fb2ac8978d70ca7c0f72eb31da551dbbac Mon Sep 17 00:00:00 2001 From: envrs Date: Thu, 4 Dec 2025 01:46:56 +0600 Subject: [PATCH 05/14] Update .gitignore Signed-off-by: envrs --- .gitignore | 70 ++++++------------------------------------------------ 1 file changed, 7 insertions(+), 63 deletions(-) diff --git a/.gitignore b/.gitignore index 67bad9cd8..cb657bfa0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,69 +1,13 @@ -# OS generated files -.DS_Store -.DS_Store? ._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Dependency directories +credentials.json node_modules/ -jspm_packages/ - -# Environment variables -.env -.env.local -.env.development.local -.env.test.local -.env.production.local - -# Build output -/dist -/build -/coverage -.nyc_output - -# IDE -.vscode/* -!.vscode/extensions.json -.idea -*.swp -*.swo -*~ - -# Project specific -config.js custom.js plugins_* output.json -*.csv -*.xlsx - -# Test results -/test-results.xml -/junit.xml - -# Package files +vscode/* +.vscode/ +.idea +coverage/ +.nyc_output +config.js package-lock.json -yarn.lock - -# Local development -.vercel -.next -.cache -.temp -.tmp From 47bf133e06322cad8ffaa464ab0be52a182cb621 Mon Sep 17 00:00:00 2001 From: envrs Date: Thu, 4 Dec 2025 01:47:19 +0600 Subject: [PATCH 06/14] Delete .prettierrc Signed-off-by: envrs --- .prettierrc | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index ef6842df4..000000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "semi": true, - "singleQuote": true, - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "trailingComma": "es5", - "bracketSpacing": true, - "arrowParens": "avoid", - "endOfLine": "lf" -} From a3bd69ea616dcb9a6b7e79200386c2e1976ce73c Mon Sep 17 00:00:00 2001 From: envrs Date: Thu, 4 Dec 2025 01:49:24 +0600 Subject: [PATCH 07/14] Update package.json Signed-off-by: envrs --- package.json | 110 ++++++++++++++------------------------------------- 1 file changed, 30 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 3534fe93b..ad99d5e59 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,16 @@ { "name": "cloudexploit", - "version": "2.1.0", + "version": "2.0.1", "description": "AWS, Azure, GCP, Oracle, GitHub security scanning scripts", "main": "index.js", - "type": "module", - "engines": { - "node": ">=16.0.0" - }, "scripts": { - "start": "node index.js", - "test": "NODE_ENV=test mocha './**/*.spec.js' --timeout 10000 --exit", - "test:watch": "nodemon --exec 'npm test' --ext 'js,json'", - "test:coverage": "nyc --reporter=html --reporter=text --reporter=lcov mocha './**/*.spec.js' --timeout 10000", - "test:ci": "npm run test -- --reporter mocha-junit-reporter --reporter-options mochaFile=./test-results.xml", - "lint": "eslint . --ext .js", - "lint:fix": "eslint . --ext .js --fix", - "audit": "npm audit --production --audit-level=moderate", - "security-check": "npm audit --production && npx snyk test", - "precommit": "npm run lint && npm test", - "prepare": "husky install" + "test": "mocha './**/*.spec.js'", + "test-watch": "nodemon --exec npm run test", + "test-cov": "nyc --reporter text --reporter cobertura --reporter lcov --all -- npm run test", + "test-cov-watch": "nodemon --exec npm run test-cov", + "test-cov-html": "nyc --reporter html --reporter cobertura --reporter lcov --all -- npm run test", + "test-cov-html-watch": "nodemon --exec npm run test-cov-html", + "lint": "npx eslint ." }, "bin": { "cloudexploit-scan": "./index.js" @@ -47,73 +39,31 @@ "access": "public" }, "dependencies": { - "@alicloud/pop-core": "^1.7.11", - "@apidevtools/json-schema-ref-parser": "^10.1.0", - "@azure/identity": "^4.0.0", - "@azure/storage-blob": "^12.17.0", - "@azure/storage-file-share": "^12.19.0", - "@azure/storage-queue": "^12.19.0", - "@azure/data-tables": "^13.3.2", - "@octokit/auth-app": "^7.0.0", - "@octokit/rest": "^21.0.0", - "ali-oss": "^6.17.0", - "argparse": "^2.0.1", - "async": "^3.2.5", - "aws-sdk": "^2.1574.0", - "axios": "^1.6.5", - "chalk": "^4.1.2", - "commander": "^11.1.0", + "@alicloud/pop-core": "^1.7.10", + "@azure/data-tables": "^13.2.2", + "@azure/storage-file-share": "^12.14.0", + "@azure/storage-queue": "^12.13.0", + "@azure/storage-blob": "^12.14.0", + "@octokit/auth-app": "^6.0.3", + "@octokit/request": "^8.1.6", + "@octokit/rest": "^20.0.2", + "ali-oss": "^6.15.2", + "argparse": "^2.0.0", + "async": "^2.6.1", + "aws-sdk": "^2.1506.0", + "azure-storage": "^2.10.3", "csv-write-stream": "^2.0.0", - "dotenv": "^16.3.1", - "fast-safe-stringify": "^2.1.1", - "google-auth-library": "^9.9.0", - "js-yaml": "^4.1.0", - "minimatch": "^9.0.3", + "fast-safe-stringify": "^2.0.6", + "google-auth-library": "^8.1.1", + "minimatch": "^3.0.4", "ms-rest-azure": "^3.0.2", - "ora": "^5.4.1", - "pino": "^8.17.1", - "pino-pretty": "^10.2.3", - "tty-table": "^4.1.3", - "winston": "^3.11.0" + "tty-table": "^4.1.3" }, "devDependencies": { - "@babel/core": "^7.23.5", - "@babel/eslint-parser": "^7.23.3", - "@babel/preset-env": "^7.23.5", - "chai": "^4.3.10", - "eslint": "^8.56.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsdoc": "^46.9.1", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-security": "^3.0.1", - "husky": "^9.0.5", - "lint-staged": "^15.2.0", - "mocha": "^10.2.0", - "mocha-junit-reporter": "^2.2.0", - "nock": "^13.3.3", - "nodemon": "^3.0.2", - "nyc": "^15.1.0", - "prettier": "^3.1.1", - "sinon": "^17.0.0", - "snyk": "^1.1252.0" - }, - "lint-staged": { - "*.js": [ - "eslint --fix", - "prettier --write" - ] - }, - "prettier": { - "semi": true, - "singleQuote": true, - "printWidth": 100, - "tabWidth": 2, - "useTabs": false, - "trailingComma": "es5", - "bracketSpacing": true, - "arrowParens": "avoid" + "chai": "4.2.0", + "eslint": "^6.8.0", + "mocha": "^6.1.4", + "nodemon": "^1.19.4", + "nyc": "^14.1.1" } } From d6e3c8f27b9e3b83629d9de013314a7c4b9483e6 Mon Sep 17 00:00:00 2001 From: envrs Date: Thu, 4 Dec 2025 01:50:52 +0600 Subject: [PATCH 08/14] Update scans_ci.yml Signed-off-by: envrs --- .github/workflows/scans_ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scans_ci.yml b/.github/workflows/scans_ci.yml index 6338651f7..a0d48c23a 100644 --- a/.github/workflows/scans_ci.yml +++ b/.github/workflows/scans_ci.yml @@ -16,10 +16,10 @@ jobs: matrix: node-version: [12.x, 14.x, 16.x] # Test on multiple Node.js versions steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' From e3ee95cf21b54eac0b444bd48854dab0a21c376d Mon Sep 17 00:00:00 2001 From: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:19:30 +0600 Subject: [PATCH 09/14] Update .github/workflows/scans_ci.yml Co-authored-by: envrs <230240030+envrs@users.noreply.github.com> Signed-off-by: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> --- .github/workflows/scans_ci.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/scans_ci.yml b/.github/workflows/scans_ci.yml index a0d48c23a..940d8ef11 100644 --- a/.github/workflows/scans_ci.yml +++ b/.github/workflows/scans_ci.yml @@ -8,18 +8,10 @@ on: schedule: - cron: '0 0 * * 0' # Weekly run on Sunday at midnight -jobs: - test: - name: Test on Node.js ${{ matrix.node-version }} - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [12.x, 14.x, 16.x] # Test on multiple Node.js versions - steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'npm' From 7cc479dd41875c0654495123cdad97f8a11e1f4d Mon Sep 17 00:00:00 2001 From: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:20:20 +0600 Subject: [PATCH 10/14] Update helpers/asl/asl-1.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> --- helpers/asl/asl-1.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helpers/asl/asl-1.js b/helpers/asl/asl-1.js index c2ec7bdc1..dddf6702e 100644 --- a/helpers/asl/asl-1.js +++ b/helpers/asl/asl-1.js @@ -825,7 +825,9 @@ var runConditions = function(input, data, results, resourcePath, resourceName, r let message = ''; // Perform the actual comparison - if (condition.op === 'CONTAINS' && propValue.includes(condition.value)) { + if (condition.op === 'CONTAINS' && + (typeof propValue === 'string' || Array.isArray(propValue)) && + propValue.includes(condition.value)) { result = 0; message = `${aliasProperty}: ${condition.value} found in ${propValue}`; foundMatch = true; From 437fcd152f791cd9c95d3912aec6fe6e640f1128 Mon Sep 17 00:00:00 2001 From: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:21:31 +0600 Subject: [PATCH 11/14] Update plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> --- plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js b/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js index 6f81c11c8..7901a01b1 100644 --- a/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js +++ b/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js @@ -39,7 +39,7 @@ module.exports = { if (!account.id) return; if (account.identity && account.identity.type && - (account.identity.type.toLowerCase() === 'systemassigned' || account.identity.type.toLowerCase() === 'userassigned')) { + (account.identity.type.toLowerCase().includes('systemassigned') || account.identity.type.toLowerCase().includes('userassigned'))) { helpers.addResult(results, 0, 'Cosmos DB account has managed identity enabled', location, account.id); } else { From bd7fe919b3d927b3651f4bfec541545bbbbeaa87 Mon Sep 17 00:00:00 2001 From: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:22:18 +0600 Subject: [PATCH 12/14] Update helpers/asl/asl-1.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> --- helpers/asl/asl-1.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/helpers/asl/asl-1.js b/helpers/asl/asl-1.js index dddf6702e..321774e40 100644 --- a/helpers/asl/asl-1.js +++ b/helpers/asl/asl-1.js @@ -167,12 +167,22 @@ var inCidrIPv6 = function(ip, cidr) { return { result: false, error: 'Malformed IP' }; } - var prefixChars = Math.floor(prefixLength / 4); - var cidrPrefix = expandedCidr.substring(0, prefixChars); - var ipPrefix = expandedIp.substring(0, prefixChars); + var fullNibbles = Math.floor(prefixLength / 4); + var remainingBits = prefixLength % 4; + + var cidrPrefix = expandedCidr.substring(0, fullNibbles); + var ipPrefix = expandedIp.substring(0, fullNibbles); var isInRange = ipPrefix === cidrPrefix; + // Compare remaining bits if prefix is not nibble-aligned + if (isInRange && remainingBits > 0 && fullNibbles < expandedCidr.length) { + var cidrNibble = parseInt(expandedCidr.charAt(fullNibbles), 16); + var ipNibble = parseInt(expandedIp.charAt(fullNibbles), 16); + var mask = (0xF << (4 - remainingBits)) & 0xF; + isInRange = (cidrNibble & mask) === (ipNibble & mask); + } + var result = { result: isInRange, error: null, From b5c4c876a37c06a2aa4284c54f8d0be97d84d236 Mon Sep 17 00:00:00 2001 From: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:32:43 +0600 Subject: [PATCH 13/14] Update plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> --- plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js b/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js index 7901a01b1..2f3fd24d7 100644 --- a/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js +++ b/plugins/azure/cosmosdb/cosmosdbManagedIdentity.spec.js @@ -7,7 +7,7 @@ module.exports = { domain: 'Databases', severity: 'Medium', description: 'Ensures that Azure Cosmos DB accounts have managed identity enabled.', - more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure AD and using it to obtain Azure Entra ID tokens.', + more_info: 'Enabling managed identities eliminate the need for developers having to manage credentials by providing an identity for the Azure resource in Azure Entra ID and using it to obtain Azure Entra ID tokens.', link: 'https://learn.microsoft.com/en-us/azure/cosmos-db/managed-identity-based-authentication', recommended_action: 'Enable system or user-assigned identities for all Azure Cosmos DB accounts.', apis: ['databaseAccounts:list'], From e8295445d28d162faac78448eba8d35abbd2f01a Mon Sep 17 00:00:00 2001 From: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:33:17 +0600 Subject: [PATCH 14/14] Update helpers/asl/asl-1.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com> --- helpers/asl/asl-1.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helpers/asl/asl-1.js b/helpers/asl/asl-1.js index 321774e40..1af0b082a 100644 --- a/helpers/asl/asl-1.js +++ b/helpers/asl/asl-1.js @@ -126,7 +126,7 @@ var inCidrIPv4 = function(ip, cidr) { var cidrInt = ((cidrParts[0] << 24) + (cidrParts[1] << 16) + (cidrParts[2] << 8) + cidrParts[3]) >>> 0; var ipInt = ((ipParts[0] << 24) + (ipParts[1] << 16) + (ipParts[2] << 8) + ipParts[3]) >>> 0; - var mask = (0xFFFFFFFF << (32 - prefixLength)) >>> 0; + var mask = prefixLength === 0 ? 0 : (0xFFFFFFFF << (32 - prefixLength)) >>> 0; var networkInt = (cidrInt & mask) >>> 0; var broadcastInt = (networkInt | (0xFFFFFFFF >>> prefixLength)) >>> 0;