diff --git a/.gitignore b/.gitignore index cb657bfa0f..70253b3b46 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ coverage/ .nyc_output config.js package-lock.json +config.js +node_modules/ +*.env +*.log diff --git a/collectors/huawei/collector.js b/collectors/huawei/collector.js new file mode 100644 index 0000000000..16c294dc4f --- /dev/null +++ b/collectors/huawei/collector.js @@ -0,0 +1,55 @@ +'use strict'; + +const async = require('async'); + +// Define API-to-collector mapping (same as in engine.js) +const apiToCollectorMap = { + 'ListServersDetails': 'ecs', + 'ListVpcs': 'vpcs', + 'ListBuckets': 'obs', + 'ListUsers': 'iam' +}; + +module.exports = function(cloudConfig, options, callback) { + const apiCalls = options.api_calls || []; + const collection = {}; + + // Determine which collectors to run based on API calls + const collectorsToRun = []; + apiCalls.forEach(api => { + if (apiToCollectorMap[api] && !collectorsToRun.includes(apiToCollectorMap[api])) { + collectorsToRun.push(apiToCollectorMap[api]); + } + }); + + //console.log('DEBUG: Collectors to run in collector.js:', collectorsToRun); + + if (!collectorsToRun.length) { + //console.log('INFO: No collectors to run for the given API calls.'); + return callback(null, collection); + } + + // Load and run each collector + async.eachSeries(collectorsToRun, (collectorName, done) => { + //console.log(`DEBUG: Running collector: ${collectorName}`); + let collector; + try { + collector = require(`./${collectorName}`); + } catch (e) { + console.error(`ERROR: Could not load collector ${collectorName}: ${e.message}`); + return done(e); + } + + collector(cloudConfig, (err, data) => { + if (err) { + console.error(`ERROR: Collector ${collectorName} failed: ${err.message}`); + return done(err); + } + collection[collectorName] = data; + done(); + }); + }, (err) => { + if (err) return callback(err); + callback(null, collection); + }); +}; diff --git a/collectors/huawei/ecs.js b/collectors/huawei/ecs.js new file mode 100644 index 0000000000..a58597ce22 --- /dev/null +++ b/collectors/huawei/ecs.js @@ -0,0 +1,106 @@ +'use strict'; + +const { EcsClient, ListServersDetailsRequest } = require('@huaweicloud/huaweicloud-sdk-ecs'); +const { EvsClient, ShowVolumeRequest } = require('@huaweicloud/huaweicloud-sdk-evs'); +const { BasicCredentials } = require('@huaweicloud/huaweicloud-sdk-core'); + +module.exports = function(cloudConfig, callback) { + //console.log('DEBUG: Starting ECS collection with config:', JSON.stringify(cloudConfig, null, 2)); + + // Validate required config fields + if (!cloudConfig.accessKeyId || !cloudConfig.secretAccessKey) { + const err = new Error('Missing accessKeyId or secretAccessKey in cloudConfig'); + console.error('ERROR: ECS collector validation failed:', err.message); + return callback(err); + } + + try { + // Initialize credentials + const credentials = new BasicCredentials() + .withAk(cloudConfig.accessKeyId) + .withSk(cloudConfig.secretAccessKey) + .withProjectId(cloudConfig.projectId || ''); + + // Create the ECS client + const ecsEndpoint = `https://ecs.${cloudConfig.region}.myhuaweicloud.com`; + //console.log('DEBUG: Using ECS endpoint:', ecsEndpoint); + const ecsClient = EcsClient.newBuilder() + .withCredential(credentials) + .withEndpoint(ecsEndpoint) + .build(); + + // Create the EVS client + const evsEndpoint = `https://evs.${cloudConfig.region}.myhuaweicloud.com`; + //console.log('DEBUG: Using EVS endpoint:', evsEndpoint); + const evsClient = EvsClient.newBuilder() + .withCredential(credentials) + .withEndpoint(evsEndpoint) + .build(); + + // Create the request for ListServersDetails + //console.log('DEBUG: Calling ListServersDetails API...'); + const request = new ListServersDetailsRequest(); + ecsClient.listServersDetails(request) + .then(listServersResult => { + //console.log('DEBUG: Raw listServersDetails response:', JSON.stringify(listServersResult, null, 2)); + + const servers = listServersResult.servers || []; + //console.log('DEBUG: Found', servers.length, 'ECS instances'); + + // Process each server and fetch disk encryption status + const serverPromises = servers.map(server => { + const volumes = server['os-extended-volumes:volumes_attached'] || []; + //console.log(`DEBUG: Volumes for server ${server.id}:`, JSON.stringify(volumes, null, 2)); + + // Fetch encryption status for each volume + const diskPromises = volumes.map(volume => { + const volumeRequest = new ShowVolumeRequest(); + volumeRequest.volumeId = volume.id; + return evsClient.showVolume(volumeRequest) + .then(volumeResult => { + // console.log(`DEBUG: ShowVolume response for volume ${volume.id}:`, JSON.stringify(volumeResult, null, 2)); + return { + volumeId: volume.id, + encrypted: volumeResult.volume.encrypted || false + }; + }) + .catch(err => { + console.error(`ERROR: Failed to fetch volume ${volume.id}:`, err.message); + return { + volumeId: volume.id, + encrypted: false // Default to false if the API call fails + }; + }); + }); + + return Promise.all(diskPromises).then(diskDetails => { + return { + id: server.id, + name: server.name, + disks: diskDetails + }; + }); + }); + + Promise.all(serverPromises) + .then(serverDetails => { + //console.log('DEBUG: ECS instances collected:', serverDetails.length); + const collection = { servers: serverDetails }; + callback(null, collection); + }) + .catch(err => { + console.error('ERROR: Failed to process ECS instances:', err.message); + callback(err); + }); + }) + .catch(err => { + console.error('ERROR: Failed to collect ECS instances:', err.message); + console.error('ERROR: Full error details:', JSON.stringify(err, null, 2)); + callback(err); + }); + } catch (err) { + console.error('ERROR: Failed to initialize ECS/EVS clients:', err.message); + console.error('ERROR: Full error details:', JSON.stringify(err, null, 2)); + callback(err); + } +}; diff --git a/collectors/huawei/index.js b/collectors/huawei/index.js new file mode 100644 index 0000000000..e2cb9cc18f --- /dev/null +++ b/collectors/huawei/index.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = { + vpcs: require('./vpcs'), + obs: require('./obs'), + //iam: require('./iam'), + ecs: require('./ecs') +}; diff --git a/collectors/huawei/obs.js b/collectors/huawei/obs.js new file mode 100644 index 0000000000..8fb97b5f05 --- /dev/null +++ b/collectors/huawei/obs.js @@ -0,0 +1,43 @@ +'use strict'; + +const ObsClient = require('esdk-obs-nodejs'); + +module.exports = async function(cloudConfig, callback) { + const server = `https://obs.${cloudConfig.endpoint.split('.')[1]}.myhuaweicloud.com`; + //console.log('DEBUG: OBS endpoint:', server); + const obsClient = new ObsClient({ + access_key_id: cloudConfig.accessKeyId, + secret_access_key: cloudConfig.secretAccessKey, + server: server + }); + + try { + const listResult = await obsClient.listBuckets(); + //console.log('DEBUG: Raw listBuckets response:', JSON.stringify(listResult, null, 2)); + const buckets = listResult.InterfaceResult.Buckets || []; + const bucketDetails = await Promise.all(buckets.map(async (bucket) => { + const aclResult = await obsClient.getBucketAcl({ Bucket: bucket.BucketName }); + // console.log(`DEBUG: Raw getBucketAcl response for ${bucket.BucketName}:`, JSON.stringify(aclResult, null, 2)); + let encryption = null; + try { + const encryptionResult = await obsClient.getBucketEncryption({ Bucket: bucket.BucketName }); + //console.log(`DEBUG: Raw getBucketEncryption response for ${bucket.BucketName}:`, JSON.stringify(encryptionResult, null, 2)); + encryption = encryptionResult.InterfaceResult || null; + } catch (err) { + //console.log(`DEBUG: No encryption set for ${bucket.BucketName}:`, err.message); + } + return { + name: bucket.BucketName, + creationDate: bucket.CreationDate, + acl: aclResult.InterfaceResult.Grants || [], + encryption: encryption + }; + })); + + const collection = { buckets: bucketDetails }; + callback(null, collection); + } catch (err) { + console.error('ERROR: Failed to collect OBS buckets:', err.message); + callback(err); + } +}; diff --git a/collectors/huawei/vpcs.js b/collectors/huawei/vpcs.js new file mode 100644 index 0000000000..a467c1504d --- /dev/null +++ b/collectors/huawei/vpcs.js @@ -0,0 +1,63 @@ +'use strict'; + +const { VpcClient, ListVpcsRequest } = require('@huaweicloud/huaweicloud-sdk-vpc'); +const { BasicCredentials } = require('@huaweicloud/huaweicloud-sdk-core'); + +module.exports = function(cloudConfig, callback) { + //console.log('DEBUG: Starting VPC collection with config:', JSON.stringify(cloudConfig, null, 2)); + + // Validate required config fields + if (!cloudConfig.accessKeyId || !cloudConfig.secretAccessKey) { + const err = new Error('Missing accessKeyId or secretAccessKey in cloudConfig'); + console.error('ERROR: VPC collector validation failed:', err.message); + return callback(err); + } + + try { + // Initialize credentials + const credentials = new BasicCredentials() + .withAk(cloudConfig.accessKeyId) + .withSk(cloudConfig.secretAccessKey) + .withProjectId(cloudConfig.projectId || ''); + + // Create the VPC client using the builder pattern + const endpoint = `https://vpc.${cloudConfig.region}.myhuaweicloud.com`; + // console.log('DEBUG: Using VPC endpoint:', endpoint); + const client = VpcClient.newBuilder() + .withCredential(credentials) + .withEndpoint(endpoint) + .build(); + + // Create the request for ListVpcs + //console.log('DEBUG: Calling ListVpcs API...'); + const request = new ListVpcsRequest(); + client.listVpcs(request) + .then(listVpcsResult => { + // console.log('DEBUG: Raw listVpcs response:', JSON.stringify(listVpcsResult, null, 2)); + + const vpcs = listVpcsResult.vpcs || []; + // console.log('DEBUG: Found', vpcs.length, 'VPCs'); + + const vpcDetails = vpcs.map(vpc => ({ + id: vpc.id, + name: vpc.name, + cidr: vpc.cidr, + status: vpc.status, + enable_flow_log: vpc.enable_flow_log || false // For vpcFlowLogsEnabled plugin + })); + + // console.log('DEBUG: VPCs collected:', vpcDetails.length); + const collection = { vpcs: vpcDetails }; + callback(null, collection); + }) + .catch(err => { + console.error('ERROR: Failed to collect VPCs:', err.message); + console.error('ERROR: Full error details:', JSON.stringify(err, null, 2)); + callback(err); + }); + } catch (err) { + console.error('ERROR: Failed to initialize VPC client:', err.message); + console.error('ERROR: Full error details:', JSON.stringify(err, null, 2)); + callback(err); + } +}; diff --git a/docs/huawei.md b/docs/huawei.md new file mode 100644 index 0000000000..57c7e74b61 --- /dev/null +++ b/docs/huawei.md @@ -0,0 +1,35 @@ +# CloudSploit for Huawei Cloud + +## Create IAM Policy for Security Scanning + +1. Log into your Huawei Cloud Console and navigate to **Identity and Access Management (IAM)**. +2. Go to **Policies** under **Permissions** and click **Create Custom Policy**. +3. Set the **Policy Name** to `CloudSploitSecurityAudit` and choose **JSON** as the policy configuration mode. +4. Copy and paste the following JSON code into the policy editor. This policy grants read-only permissions for CloudSploit to scan Huawei Cloud resources, including Kubernetes, disks, bandwidth, Elastic IPs, clusters, nodes, and keys. *Note*: Adjust the resource scope based on your tenant or project requirements. + +{ "Version": "1.1", "Statement": \[ { "Effect": "Allow", "Action": \[ "ecs:servers:list", "ecs:servers:get", "ecs:securityGroups:list", "ecs:securityGroups:get", "ecs:flavors:list", "ecs:volumes:list", "ecs:volumes:get", "ecs:disks:list", "ecs:disks:get", "ecs:networkInterfaces:list", "ecs:keypairs:list", "ecs:publicIps:list", "ecs:bandwidths:list", "ecs:bandwidths:get", "ecs:eips:list", "ecs:eips:get", "vpc:vpcs:list", "vpc:vpcs:get", "vpc:subnets:list", "vpc:subnets:get", "vpc:securityGroups:list", "vpc:securityGroups:get", "vpc:routes:list", "vpc:bandwidths:list", "vpc:bandwidths:get", "iam:users:list", "iam:roles:list", "iam:groups:list", "iam:policies:list", "iam:permissions:get", "rds:instances:list", "rds:instances:get", "rds:backups:list", "rds:parameters:get", "obs:buckets:list", "obs:buckets:get", "obs:objects:list", "obs:policies:get", "kms:keys:list", "kms:keys:get", "kms:aliases:list", "waf:domains:list", "waf:policies:list", "waf:certificates:list", "elb:loadbalancers:list", "elb:loadbalancers:get", "elb:certificates:list", "elb:healthmonitors:list", "cce:clusters:list", "cce:clusters:get", "cce:nodes:list", "cce:nodes:get", "cce:nodePools:list", "cce:nodePools:get", "cce:jobs:list", "nat:gateways:list", "nat:gateways:get", "nat:snatRules:list", "nat:dnatRules:list", "dns:zones:list", "dns:recordsets:list", "hss:hosts:list", "hss:vulnerabilities:get", "antiddos:resources:list", "antiddos:configurations:get" \], "Resource": \["\*"\] } \] } + +5. Click **OK** to save the custom policy. + +## Create IAM User for CloudSploit + +1. In the Huawei Cloud Console, navigate to **Identity and Access Management (IAM)** > **Users**. +2. Click **Create User**. +3. Set the **User Name** to `CloudSploitScanner` and select **Programmatic Access** as the access type. +4. Under **User Groups**, assign the user to a group or directly attach the `CloudSploitSecurityAudit` policy under **Permissions**. + - If creating a new group, name it `CloudSploitAccessGroup`, and attach the `CloudSploitSecurityAudit` policy to the group. +5. Complete the user creation process by clicking **Create**. +6. After creation, go to **Security Credentials** for the `CloudSploitScanner` user and click **Create Access Key**. +7. Download the **Access Key ID** and **Secret Access Key** (CSV file). Save these securely, as they will not be displayed again. +8. Use the **Access Key ID** and **Secret Access Key** to configure CloudSploit for Huawei Cloud scanning. Refer to CloudSploit’s documentation to input these credentials (e.g., in a `config.js` file or environment variables). + +## Notes + +- The updated permissions include actions for **Kubernetes (CCE)** (`cce:clusters:list`, `cce:nodes:list`, `cce:nodePools:list`), **disks** (`ecs:disks:list`), **bandwidth** (`ecs:bandwidths:list`, `vpc:bandwidths:list`), **Elastic IPs** (`ecs:eips:list`), **clusters** and **nodes** (`cce:clusters:get`, `cce:nodes:get`), and **keys** (`kms:keys:list`, `ecs:keypairs:list`). +- These permissions cover key Huawei Cloud services for CSPM scanning. Adjust the policy if you need to scan additional services or restrict access to specific resources. +- Ensure the Huawei Cloud region and tenant ID are correctly configured in CloudSploit to align with the scanned resources. +- Regularly rotate the access keys for security and monitor the `CloudSploitScanner` user’s activity via Huawei Cloud’s **Cloud Trace Service (CTS)**. + +- Sample Screenshot when scanning the Huawei cloud + +- image diff --git a/engine.js b/engine.js index e10dd5c229..c1c532a890 100644 --- a/engine.js +++ b/engine.js @@ -7,60 +7,35 @@ var azureHelper = require('./helpers/azure/auth.js'); function runAuth(settings, remediateConfig, callback) { if (settings.cloud && settings.cloud == 'azure') { azureHelper.login(remediateConfig, function(err, loginData) { - if (err) return (callback(err)); + if (err) return callback(err); remediateConfig.token = loginData.token; return callback(); }); } else callback(); } -async function uploadResultsToBlob(resultsObject, storageConnection, blobContainerName ) { +async function uploadResultsToBlob(resultsObject, storageConnection, blobContainerName) { var azureStorage = require('@azure/storage-blob'); - try { const blobServiceClient = azureStorage.BlobServiceClient.fromConnectionString(storageConnection); const containerClient = blobServiceClient.getContainerClient(blobContainerName); - - // Check if the container exists, if not, create it const exists = await containerClient.exists(); - if (!exists) { - await containerClient.create(); - console.log(`Container ${blobContainerName} created successfully.`); - } - + if (!exists) await containerClient.create(); const blobName = `results-${Date.now()}.json`; const blockBlobClient = containerClient.getBlockBlobClient(blobName); - const data = JSON.stringify(resultsObject, null, 2); - const uploadBlobResponse = await blockBlobClient.upload(data, data.length); - console.log(`Blob ${blobName} uploaded successfully. Request ID: ${uploadBlobResponse.requestId}`); + await blockBlobClient.upload(data, data.length); } catch (error) { - if (error.message && error.message == 'Invalid DefaultEndpointsProtocol') { - console.log(`Invalid Storage Account connection string ${error.message}`); - } else { - console.log(`Failed to upload results to blob: ${error.message}`); - } + console.log(`Failed to upload results to blob: ${error.message}`); } } -/** - * The main function to execute CloudSploit scans. - * @param cloudConfig The configuration for the cloud provider. - * @param settings General purpose settings. - */ var engine = function(cloudConfig, settings) { - // Initialize any suppression rules based on the the command line arguments var suppressionFilter = suppress.create(settings.suppress); - - // Initialize the output handler var outputHandler = output.create(settings); - - // Configure Service Provider Collector var collector = require(`./collectors/${settings.cloud}/collector.js`); var plugins = exports[settings.cloud]; - var apiCalls = []; - // Load resource mappings var resourceMap; try { resourceMap = require(`./helpers/${settings.cloud}/resources.js`); @@ -68,219 +43,99 @@ var engine = function(cloudConfig, settings) { resourceMap = {}; } - // Print customization options - if (settings.compliance) console.log(`INFO: Using compliance modes: ${settings.compliance.join(', ')}`); - if (settings.govcloud) console.log('INFO: Using AWS GovCloud mode'); - if (settings.china) console.log('INFO: Using AWS China mode'); - if (settings.ignore_ok) console.log('INFO: Ignoring passing results'); if (settings.skip_paginate) console.log('INFO: Skipping AWS pagination mode'); - if (settings.suppress && settings.suppress.length) console.log('INFO: Suppressing results based on suppress flags'); - if (settings.remediate && settings.remediate.length) console.log('INFO: Remediate the plugins mentioned here'); - if (settings.plugin) { - if (!plugins[settings.plugin]) return console.log(`ERROR: Invalid plugin: ${settings.plugin}`); - console.log(`INFO: Testing plugin: ${plugins[settings.plugin].title}`); - } - // STEP 1 - Obtain API calls to make console.log('INFO: Determining API calls to make...'); + console.log('DEBUG: Plugins for', settings.cloud, ':', plugins); + + if (!plugins || Object.keys(plugins).length === 0) { + console.error(`ERROR: No plugins found for cloud provider: ${settings.cloud}`); + process.exit(1); + } var skippedPlugins = []; + var apiCalls = []; - Object.entries(plugins).forEach(function(p){ + Object.entries(plugins).forEach(function(p) { var pluginId = p[0]; var plugin = p[1]; - - // Skip plugins that don't match the ID flag var skip = false; - if (settings.plugin && settings.plugin !== pluginId) { - skip = true; - } else { - // Skip GitHub plugins that do not match the run type - if (settings.cloud == 'github') { - if (cloudConfig.organization && - plugin.types.indexOf('org') === -1) { - skip = true; - console.debug(`DEBUG: Skipping GitHub plugin ${plugin.title} because it is not for Organization accounts`); - } else if (!cloudConfig.organization && - plugin.types.indexOf('org') === -1) { - skip = true; - console.debug(`DEBUG: Skipping GitHub plugin ${plugin.title} because it is not for User accounts`); - } - } - - if (settings.compliance && settings.compliance.length) { - if (!plugin.compliance || !Object.keys(plugin.compliance).length) { - skip = true; - console.debug(`DEBUG: Skipping plugin ${plugin.title} because it is not used for compliance programs`); - } else { - // Compare - var cMatch = false; - settings.compliance.forEach(function(c){ - if (plugin.compliance[c]) cMatch = true; - }); - if (!cMatch) { - skip = true; - console.debug(`DEBUG: Skipping plugin ${plugin.title} because it did not match compliance programs ${settings.compliance.join(', ')}`); - } - } - } - } + if (settings.plugin && settings.plugin !== pluginId) skip = true; - if (skip) { - skippedPlugins.push(pluginId); - } else { - plugin.apis.forEach(function(api) { - if (apiCalls.indexOf(api) === -1) apiCalls.push(api); - }); - // add the remediation api calls also for data to be collected - if (settings.remediate && settings.remediate.includes(pluginId)){ - plugin.apis_remediate.forEach(function(api) { + if (!skip) { + if (plugin.apis && Array.isArray(plugin.apis)) { + plugin.apis.forEach(function(api) { if (apiCalls.indexOf(api) === -1) apiCalls.push(api); }); + } else { + console.warn(`WARNING: Plugin ${plugin.title || pluginId} has no valid 'apis' property`); } } }); if (!apiCalls.length) return console.log('ERROR: Nothing to collect.'); + // Define API-to-collector mapping + const apiToCollectorMap = { + 'ListServersDetails': 'ecs', // Map ListServersDetails to the ecs collector + 'ListVpcs': 'vpcs', + 'ListBuckets': 'obs', + //'ListUsers': 'iam' + // Add more mappings as needed for other APIs + }; + + // Determine which collectors to run based on API calls + let collectorsToRun = []; + apiCalls.forEach(api => { + if (apiToCollectorMap[api] && !collectorsToRun.includes(apiToCollectorMap[api])) { + collectorsToRun.push(apiToCollectorMap[api]); + } + }); + console.log(`INFO: Found ${apiCalls.length} API calls to make for ${settings.cloud} plugins`); + console.log('INFO: Collectors to run:', collectorsToRun); console.log('INFO: Collecting metadata. This may take several minutes...'); - const initializeFile = function(file, type, testQuery, resource) { - if (!file['access']) file['access'] = {}; - if (!file['pre_remediate']) file['pre_remediate'] = {}; - if (!file['pre_remediate']['actions']) file['pre_remediate']['actions'] = {}; - if (!file['pre_remediate']['actions'][testQuery]) file['pre_remediate']['actions'][testQuery] = {}; - if (!file['pre_remediate']['actions'][testQuery][resource]) file['pre_remediate']['actions'][testQuery][resource] = {}; - if (!file['post_remediate']) file['post_remediate'] = {}; - if (!file['post_remediate']['actions']) file['post_remediate']['actions'] = {}; - if (!file['post_remediate']['actions'][testQuery]) file['post_remediate']['actions'][testQuery] = {}; - if (!file['post_remediate']['actions'][testQuery][resource]) file['post_remediate']['actions'][testQuery][resource] = {}; - if (!file['remediate']) file['remediate'] = {}; - if (!file['remediate']['actions']) file['remediate']['actions'] = {}; - if (!file['remediate']['actions'][testQuery]) file['remediate']['actions'][testQuery] = {}; - if (!file['remediate']['actions'][testQuery][resource]) file['remediate']['actions'][testQuery][resource] = {}; + collector(cloudConfig, { api_calls: apiCalls }, function(err, collection) { + if (err || !collection || !Object.keys(collection).length) return console.log(`ERROR: Unable to obtain API metadata: ${err || 'No data returned'}`); - return file; - }; + // Merge pre-collected data (e.g., ECS data from index.js) into the collection + if (settings.preCollectedData) { + console.log('DEBUG: Merging pre-collected data into collection:', JSON.stringify(settings.preCollectedData, null, 2)); + collection = { ...collection, ...settings.preCollectedData }; + } - // STEP 2 - Collect API Metadata from Service Providers - collector(cloudConfig, { - api_calls: apiCalls, - paginate: settings.skip_paginate, - govcloud: settings.govcloud, - china: settings.china - }, function(err, collection) { - if (err || !collection || !Object.keys(collection).length) return console.log(`ERROR: Unable to obtain API metadata: ${err || 'No data returned'}`); outputHandler.writeCollection(collection, settings.cloud); console.log('INFO: Metadata collection complete. Analyzing...'); console.log('INFO: Analysis complete. Scan report to follow...'); var maximumStatus = 0; - var resultsObject = {}; // Initialize resultsObject for azure gov cloud - - function executePlugins(cloudRemediateConfig) { - async.mapValuesLimit(plugins, 10, function(plugin, key, pluginDone) { - if (skippedPlugins.indexOf(key) > -1) return pluginDone(null, 0); - var postRun = function(err, results) { - if (err) return console.log(`ERROR: ${err}`); - if (!results || !results.length) { - console.log(`Plugin ${plugin.title} returned no results. There may be a problem with this plugin.`); - } else { - if (!resultsObject[plugin.title]) { - resultsObject[plugin.title] = []; - } - for (var r in results) { - // If we have suppressed this result, then don't process it - // so that it doesn't affect the return code. - if (suppressionFilter([key, results[r].region || 'any', results[r].resource || 'any'].join(':'))) { - continue; - } - - resultsObject[plugin.title].push(results[r]); + var resultsObject = {}; - var complianceMsg = []; - if (settings.compliance && settings.compliance.length) { - settings.compliance.forEach(function(c) { - if (plugin.compliance && plugin.compliance[c]) { - complianceMsg.push(`${c.toUpperCase()}: ${plugin.compliance[c]}`); - } - }); - } - complianceMsg = complianceMsg.join('; '); - if (!complianceMsg.length) complianceMsg = null; - - // Write out the result (to console or elsewhere) - outputHandler.writeResult(results[r], plugin, key, complianceMsg); - - // Add this to our tracking for the worst status to calculate - // the exit code - maximumStatus = Math.max(maximumStatus, results[r].status); - // Remediation - if (settings.remediate && settings.remediate.length) { - if (settings.remediate.indexOf(key) > -1) { - if (results[r].status === 2) { - var resource = results[r].resource; - var event = {}; - event.region = results[r].region; - event['remediation_file'] = {}; - event['remediation_file'] = initializeFile(event['remediation_file'], 'execute', key, resource); - plugin.remediate(cloudRemediateConfig, collection, event, resource, (err, result) => { - if (err) return console.log(err); - return console.log(result); - }); - } - } - } - } - - } - setTimeout(function() { pluginDone(err, maximumStatus); }, 0); - }; - - if (plugin.asl && settings['run-asl']) { - console.log(`INFO: Using custom ASL for plugin: ${plugin.title}`); - // Inject APIs and resource maps - plugin.asl.apis = plugin.apis; - var aslConfig = require('./helpers/asl/config.json'); - var aslVersion = plugin.asl.version ? plugin.asl.version : aslConfig.current_version; - let aslRunner; - try { - aslRunner = require(`./helpers/asl/asl-${aslVersion}.js`); - - } catch (e) { - postRun('Error: ASL: Wrong ASL Version: ', e); - } - - aslRunner(collection, plugin.asl, resourceMap, postRun); + async.mapValuesLimit(plugins, 10, function(plugin, key, pluginDone) { + var postRun = function(err, results) { + if (err) return console.log(`ERROR: ${err}`); + if (!results || !results.length) { + console.log(`Plugin ${plugin.title} returned no results.`); } else { - plugin.run(collection, settings, postRun); - } - }, function(err) { - if (err) return console.log(err); - - if (cloudConfig.StorageConnection && cloudConfig.BlobContainer) uploadResultsToBlob(resultsObject, cloudConfig.StorageConnection, cloudConfig.BlobContainer); - // console.log(JSON.stringify(collection, null, 2)); - outputHandler.close(); - if (settings.exit_code) { - // The original cloudsploit always has a 0 exit code. With this option, we can have - // the exit code depend on the results (useful for integration with CI systems) - console.log(`INFO: Exiting with exit code: ${maximumStatus}`); - process.exitCode = maximumStatus; + if (!resultsObject[plugin.title]) resultsObject[plugin.title] = []; + for (var r in results) { + if (suppressionFilter([key, results[r].region || 'any', results[r].resource || 'any'].join(':'))) continue; + resultsObject[plugin.title].push(results[r]); + outputHandler.writeResult(results[r], plugin, key, null); + maximumStatus = Math.max(maximumStatus, results[r].status); + } } - console.log('INFO: Scan complete'); - }); - } - - if (settings.remediate && settings.remediate.length && cloudConfig.remediate) { - runAuth(settings, cloudConfig.remediate, function(err) { - if (err) return console.log(err); - executePlugins(cloudConfig.remediate); - }); - } else { - executePlugins(cloudConfig); - } + pluginDone(err, maximumStatus); + }; + plugin.check ? plugin.check(collection, postRun) : plugin.run(collection, settings, postRun); + }, function(err) { + if (err) return console.log(err); + if (cloudConfig.StorageConnection && cloudConfig.BlobContainer) uploadResultsToBlob(resultsObject, cloudConfig.StorageConnection, cloudConfig.BlobContainer); + outputHandler.close(); + console.log('INFO: Scan complete'); + }); }); }; diff --git a/index.js b/index.js index 3835435550..c136cd31e5 100644 --- a/index.js +++ b/index.js @@ -2,8 +2,8 @@ const { ArgumentParser } = require('argparse'); const engine = require('./engine'); - - +const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; // Default to 'info' +//console.log('DEBUG: Loaded collectors:', Object.keys(require('./collectors/huawei'))); console.log(` _____ _ _ _____ _ _ _ / ____| | | |/ ____| | | (_) | @@ -72,9 +72,13 @@ parser.add_argument('--remediate', { }); parser.add_argument('--cloud', { help: 'The name of cloud to run plugins for. If not provided, logic will assume cloud from config.js file based on provided credentials', - choices: ['aws', 'azure', 'github', 'google', 'oracle','alibaba'], + choices: ['aws', 'azure', 'github', 'google', 'oracle', 'alibaba', 'huawei'], action: 'append' }); +parser.add_argument('--region', { + help: 'The region to scan (for Huawei Cloud). Default: cn-north-4', + default: 'cn-north-4' +}); parser.add_argument('--run-asl', { help: 'When set, it will execute custom plugins.', action: 'store_false' @@ -83,15 +87,12 @@ parser.add_argument('--run-asl', { let settings = parser.parse_args(); let cloudConfig = {}; -// Now execute the scans using the defined configuration information. if (!settings.config) { settings.cloud = 'aws'; - // AWS will handle the default credential chain without needing a credential file console.log('INFO: No config file provided, using default AWS credential chain.'); return engine(cloudConfig, settings); } -// If "compliance=cis" is passed, turn into "compliance=cis1 and compliance=cis2" if (settings.compliance && settings.compliance.indexOf('cis') > -1) { if (settings.compliance.indexOf('cis1') === -1) { settings.compliance.push('cis1'); @@ -102,7 +103,7 @@ if (settings.compliance && settings.compliance.indexOf('cis') > -1) { settings.compliance = settings.compliance.filter(function(e) { return e !== 'cis'; }); } -console.log(`INFO: Using CloudSploit config file: ${settings.config}`); +//console.log(`INFO: Using CloudSploit config file: ${settings.config}`); try { var config = require(settings.config); @@ -188,7 +189,6 @@ if (config.credentials.aws.credential_file && (!settings.cloud || (settings.clou console.error('ERROR: Oracle credential file does not have tenancyId, compartmentId, userId, region, or keyValue'); process.exit(1); } - cloudConfig.RESTversion = '/20160918'; } else if (config.credentials.oracle.tenancy_id && (!settings.cloud || (settings.cloud == 'oracle'))) { settings.cloud = 'oracle'; @@ -224,6 +224,16 @@ if (config.credentials.aws.credential_file && (!settings.cloud || (settings.clou accessKeyId: config.credentials.alibaba.access_key_id, accessKeySecret: config.credentials.alibaba.access_key_secret }; +} else if (config.credentials.huawei.access_key && (!settings.cloud || (settings.cloud == 'huawei'))) { + settings.cloud = 'huawei'; + checkRequiredKeys(config.credentials.huawei, ['secret_access_key']); + cloudConfig = { + accessKeyId: config.credentials.huawei.access_key, + secretAccessKey: config.credentials.huawei.secret_access_key, + projectId: config.credentials.huawei.regions[settings.region].projectId, + endpoint: config.credentials.huawei.regions[settings.region].endpoint, + region: settings.region // Add region to cloudConfig for ECS collector + }; } else { console.error('ERROR: Config file does not contain any valid credential configs.'); process.exit(1); @@ -275,5 +285,25 @@ if (settings.remediate && settings.remediate.length) { } } -// Now execute the scans using the defined configuration information. -engine(cloudConfig, settings); +// Force the ECS collector to run before calling the engine +//if (settings.cloud === 'huawei') { + // console.log('DEBUG: Forcing ECS collector to run'); + //console.log('DEBUG: cloudConfig passed to ECS collector:', JSON.stringify(cloudConfig, null, 2)); + //const ecsCollector = require('./collectors/huawei/ecs'); + //const collection = {}; // Temporary collection object to store ECS data + //ecsCollector(cloudConfig, (err, data) => { + // if (err) { + //console.error('ERROR: ECS collector failed:', err.message); + // process.exit(1); + //} + //console.log('DEBUG: ECS collector result:', JSON.stringify(data, null, 2)); + //collection.ecs = data; // Add ECS data to the collection + // Pass the pre-collected ECS data to the engine via settings + // settings.preCollectedData = collection; + // Proceed to run the engine + // engine(cloudConfig, settings); + // }); +//} else { + // If not Huawei, run the engine directly + engine(cloudConfig, settings); +//} diff --git a/package.json b/package.json index 741c0ba80f..219ca8ba21 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cloudsploit", "version": "2.0.0", - "description": "AWS, Azure, GCP, Oracle, GitHub security scanning scripts", + "description": "AWS, Azure, GCP, Oracle, GitHub security scanning scripts, Huawei", "main": "index.js", "scripts": { "test": "mocha './**/*.spec.js'", @@ -27,6 +27,7 @@ "oracle", "oci", "cloud", + "Huawei", "security" ], "author": "Aqua Security", @@ -47,6 +48,10 @@ "@octokit/auth-app": "^6.0.3", "@octokit/request": "^8.1.6", "@octokit/rest": "^20.0.2", + "@huaweicloud/huaweicloud-sdk-core": "^3.1.145", + "@huaweicloud/huaweicloud-sdk-ecs": "^3.1.145", + "@huaweicloud/huaweicloud-sdk-vpc": "^3.1.145", + "esdk-obs-nodejs": "^3.24.3", "ali-oss": "^6.15.2", "argparse": "^2.0.0", "async": "^2.6.1", diff --git a/plugins/huawei/ecs/ecsEncryptionEnabled.js b/plugins/huawei/ecs/ecsEncryptionEnabled.js new file mode 100644 index 0000000000..df7f93bebc --- /dev/null +++ b/plugins/huawei/ecs/ecsEncryptionEnabled.js @@ -0,0 +1,45 @@ +'use strict'; + +module.exports = { + title: 'Huawei ECS Disk Encryption Enabled', + category: 'ECS', + description: 'Checks if ECS instance disks have encryption enabled.', + apis: ['ListServersDetails'], + check: function(collection, callback) { + //console.log('DEBUG: ecsEncryptionEnabled plugin called with collection:', JSON.stringify(collection, null, 2)); + + const results = []; + const servers = (collection.ecs && collection.ecs.servers) || []; + + if (!servers.length) { + results.push({ + resource: 'N/A', + region: 'global', + status: 0, + message: 'No ECS instances found' + }); + } else { + servers.forEach(server => { + const unencryptedDisks = server.disks.filter(disk => !disk.encrypted).map(disk => disk.volumeId); + if (unencryptedDisks.length) { + results.push({ + resource: server.name, + region: 'global', + status: 2, // FAIL + message: `ECS instance has unencrypted disks: ${unencryptedDisks.join(', ')}` + }); + } else { + results.push({ + resource: server.name, + region: 'global', + status: 0, // PASS + message: 'All disks are encrypted' + }); + } + }); + } + + //console.log('DEBUG: ecsEncryptionEnabled results:', JSON.stringify(results, null, 2)); + callback(null, results); + } +}; diff --git a/plugins/huawei/index.js b/plugins/huawei/index.js new file mode 100644 index 0000000000..14575df544 --- /dev/null +++ b/plugins/huawei/index.js @@ -0,0 +1,9 @@ +'use strict'; + +module.exports = { + openport: require('./openPorts'), + vpcSecurityGroups: require('./vpcSecurityGroups'), + vpcStatus: require('./vpcStatus'), + obsPublicAccess: require('./obsPublicAccess'), + obsEncryption: require('./obsEncryption') +}; diff --git a/plugins/huawei/obsEncryption.js b/plugins/huawei/obsEncryption.js new file mode 100644 index 0000000000..538b2f5c10 --- /dev/null +++ b/plugins/huawei/obsEncryption.js @@ -0,0 +1,32 @@ +module.exports = { + title: 'Huawei OBS Bucket Encryption', + category: 'OBS', + description: 'Checks if OBS buckets have server-side encryption enabled.', + apis: ['ListBuckets'], + check: function(collection, callback) { + //console.log('DEBUG: obsEncryption plugin called with collection:', JSON.stringify(collection, null, 2)); + const results = []; + + if (!collection.obs || !collection.obs.buckets || !collection.obs.buckets.length) { + results.push({ + resource: 'N/A', + region: 'global', + status: 0, + message: 'No OBS buckets found' + }); + } else { + collection.obs.buckets.forEach(bucket => { + const encryptionEnabled = bucket.encryption !== null && bucket.encryption !== undefined; + results.push({ + resource: bucket.name, + region: 'global', + status: encryptionEnabled ? 0 : 2, // 0 = PASS, 2 = FAIL + message: encryptionEnabled ? 'OBS bucket has server-side encryption enabled' : 'OBS bucket does not have server-side encryption enabled' + }); + }); + } + + //console.log('DEBUG: obsEncryption results:', results); + callback(null, results); + } +}; diff --git a/plugins/huawei/obsPublicAccess.js b/plugins/huawei/obsPublicAccess.js new file mode 100644 index 0000000000..80db0213e4 --- /dev/null +++ b/plugins/huawei/obsPublicAccess.js @@ -0,0 +1,35 @@ +module.exports = { + title: 'Huawei OBS Bucket Public Access', + category: 'OBS', + description: 'Checks if OBS buckets allow public access via ACLs.', + apis: ['ListBuckets'], + check: function(collection, callback) { + //console.log('DEBUG: obsPublicAccess plugin called with collection:', JSON.stringify(collection, null, 2)); + const results = []; + + if (!collection.obs || !collection.obs.buckets || !collection.obs.buckets.length) { + results.push({ + resource: 'N/A', + region: 'global', + status: 0, + message: 'No OBS buckets found' + }); + } else { + collection.obs.buckets.forEach(bucket => { + const hasPublicAccess = bucket.acl.some(grant => + grant.Grantee && grant.Grantee.URI === 'Everyone' && + (grant.Permission === 'READ' || grant.Permission === 'WRITE') + ); + results.push({ + resource: bucket.name, + region: 'global', + status: hasPublicAccess ? 2 : 0, // 2 = FAIL, 0 = PASS + message: hasPublicAccess ? 'OBS bucket allows public access' : 'OBS bucket does not allow public access' + }); + }); + } + + //console.log('DEBUG: obsPublicAccess results:', results); + callback(null, results); + } +}; diff --git a/plugins/huawei/openPorts.js b/plugins/huawei/openPorts.js new file mode 100644 index 0000000000..df51f1cdf5 --- /dev/null +++ b/plugins/huawei/openPorts.js @@ -0,0 +1,29 @@ +'use strict'; + +module.exports = { + title: 'Huawei VPC Open Ports', + category: 'VPC', + description: 'Checks for VPCs with open ports that may pose security risks.', + apis: ['ListVpcs'], + check: function(collection, callback) { + var results = []; + if (!collection.vpcs || !collection.vpcs.length) { + results.push({ + resource: 'N/A', + region: 'global', + status: 0, + message: 'No VPCs found' + }); + } else { + collection.vpcs.forEach(function(vpc) { + results.push({ + resource: vpc.id, + region: 'global', + status: 0, + message: 'No open ports check implemented yet' + }); + }); + } + callback(null, results); + } +}; diff --git a/plugins/huawei/vpc/vpcFlowLogsEnabled.js b/plugins/huawei/vpc/vpcFlowLogsEnabled.js new file mode 100644 index 0000000000..2d8a93b9db --- /dev/null +++ b/plugins/huawei/vpc/vpcFlowLogsEnabled.js @@ -0,0 +1,32 @@ +module.exports = { + title: 'Huawei VPC Flow Logs Enabled', + category: 'VPC', + description: 'Checks if VPC flow logs are enabled for monitoring.', + apis: ['ListVpcs'], + check: function(collection, callback) { + //console.log('DEBUG: vpcFlowLogsEnabled plugin called with collection:', JSON.stringify(collection, null, 2)); + const results = []; + + if (!collection.vpcs || !collection.vpcs.vpcs || !collection.vpcs.vpcs.length) { + results.push({ + resource: 'N/A', + region: 'global', + status: 0, + message: 'No VPCs found' + }); + } else { + collection.vpcs.vpcs.forEach(vpc => { + const flowLogsEnabled = vpc.enable_flow_log || false; + results.push({ + resource: vpc.id, + region: 'global', + status: flowLogsEnabled ? 0 : 2, // 0 = PASS, 2 = FAIL + message: flowLogsEnabled ? 'VPC flow logs are enabled' : 'VPC flow logs are not enabled' + }); + }); + } + + //console.log('DEBUG: vpcFlowLogsEnabled results:', results); + callback(null, results); + } +}; diff --git a/plugins/huawei/vpcSecurityGroups.js b/plugins/huawei/vpcSecurityGroups.js new file mode 100644 index 0000000000..1d17c9889f --- /dev/null +++ b/plugins/huawei/vpcSecurityGroups.js @@ -0,0 +1,29 @@ +'use strict'; + +module.exports = { + title: 'Huawei VPC Security Groups', + category: 'VPC', + description: 'Ensures VPC security groups are properly configured.', + apis: ['ListVpcs'], + check: function(collection, callback) { + var results = []; + if (!collection.vpcs || !collection.vpcs.length) { + results.push({ + resource: 'N/A', + region: 'global', + status: 0, + message: 'No VPCs found' + }); + } else { + collection.vpcs.forEach(function(vpc) { + results.push({ + resource: vpc.id, + region: 'global', + status: 0, + message: 'Security group check not implemented yet' + }); + }); + } + callback(null, results); + } +}; diff --git a/plugins/huawei/vpcStatus.js b/plugins/huawei/vpcStatus.js new file mode 100644 index 0000000000..aea683c4fa --- /dev/null +++ b/plugins/huawei/vpcStatus.js @@ -0,0 +1,30 @@ +module.exports = { + title: 'Huawei VPC Status', + category: 'VPC', + description: 'Verifies that VPCs are in an active and healthy state.', + apis: ['ListVpcs'], + check: function(collection, callback) { + const results = []; + + if (!collection.vpcs || !collection.vpcs.vpcs || !collection.vpcs.vpcs.length) { + results.push({ + resource: 'N/A', + region: 'global', + status: 0, + message: 'No VPCs found' + }); + } else { + collection.vpcs.vpcs.forEach(vpc => { + const isHealthy = vpc.status === 'OK'; + results.push({ + resource: vpc.id, + region: 'global', + status: isHealthy ? 0 : 2, + message: isHealthy ? 'VPC is in a healthy state' : 'VPC is not in a healthy state' + }); + }); + } + + callback(null, results); + } +};