diff --git a/libs/hdf-converters/package.json b/libs/hdf-converters/package.json index 7feb46be28..cc4735324e 100644 --- a/libs/hdf-converters/package.json +++ b/libs/hdf-converters/package.json @@ -23,6 +23,7 @@ "dependencies": { "@types/csv2json": "^1.4.2", "@types/xml2js": "^0.4.9", + "aws-sdk": "^2.1046.0", "axios": "^0.24.0", "csv-parse": "^5.0.1", "csv2json": "^2.0.2", diff --git a/libs/hdf-converters/src/aws-config-mapper.ts b/libs/hdf-converters/src/aws-config-mapper.ts new file mode 100644 index 0000000000..ada4fb8135 --- /dev/null +++ b/libs/hdf-converters/src/aws-config-mapper.ts @@ -0,0 +1,454 @@ +import { + ComplianceByConfigRule, + ConfigRule, + DescribeConfigRulesCommandInput, + EvaluationResult +} from '@aws-sdk/client-config-service'; +import AWS from 'aws-sdk'; +import https from 'https'; +import {ExecJSON} from 'inspecjs'; +import _ from 'lodash'; +import {version as HeimdallToolsVersion} from '../package.json'; +import {AwsConfigMapping} from './mappings/AwsConfigMapping'; + +const NOT_APPLICABLE_MSG = + 'No AWS resources found to evaluate compliance for this rule'; +const INSUFFICIENT_DATA_MSG = + 'Not enough data has been collected to determine compliance yet.'; +const NAME = 'AWS Config'; + +const AWS_CONFIG_MAPPING = new AwsConfigMapping(); + +export class AwsConfigMapper { + configService: AWS.ConfigService; + issues: Promise; + results: ExecJSON.ControlResult[][]; + constructor( + options: AWS.ConfigService.ClientConfiguration, + verifySSLCertificates = true + ) { + AWS.config.update({ + httpOptions: { + agent: new https.Agent({ + rejectUnauthorized: verifySSLCertificates + }) + } + }); + this.configService = new AWS.ConfigService(options); + this.results = []; + this.issues = this.getAllConfigRules(); + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + private async getAllConfigRules(): Promise { + let params: DescribeConfigRulesCommandInput = { + ConfigRuleNames: [], + NextToken: '' + }; + const configRules: ConfigRule[] = []; + let response = await this.getConfigRulePage(params); + if (response.ConfigRules === undefined) { + throw new Error('No data was returned'); + } else { + while (response !== undefined && response.ConfigRules !== undefined) { + response.ConfigRules.forEach((rule) => { + configRules.push(rule); + }); + if (response.NextToken) { + params = _.set(params, 'NextToken', response.NextToken); + } else { + break; + } + response = await this.getConfigRulePage(params); + } + } + this.results = await this.getResults(configRules); + return configRules; + } + + private chunkArray(sourceArray: Array, chunkSize: number) { + const result = []; + for (let i = 0; i < sourceArray.length; i += chunkSize) { + result.push(sourceArray.slice(i, i + chunkSize)); + } + return result; + } + + private async getConfigRulePage( + params: DescribeConfigRulesCommandInput + ): Promise { + await this.delay(150); + return this.configService.describeConfigRules(params).promise(); + } + + private async getResults( + configRules: ConfigRule[] + ): Promise { + const complianceResults: ComplianceByConfigRule[] = + await this.fetchAllComplianceInfo(configRules); + const ruleData: ExecJSON.ControlResult[][] = []; + const allRulesResolved: AWS.ConfigService.EvaluationResults = []; + for (const configRule of configRules) { + const result: ExecJSON.ControlResult[] = []; + let params = { + ConfigRuleName: configRule.ConfigRuleName || '', + Limit: 100 + }; + await this.delay(150); + let response = await this.configService + .getComplianceDetailsByConfigRule(params) + .promise(); + let ruleResults = response.EvaluationResults || []; + allRulesResolved.push(...ruleResults); + while (response.NextToken !== undefined) { + params = _.set(params, 'NextToken', response.NextToken); + await this.delay(150); + response = await this.configService + .getComplianceDetailsByConfigRule(params) + .promise(); + ruleResults = ruleResults?.concat(response.EvaluationResults || []); + allRulesResolved.push(...ruleResults); + } + ruleResults.forEach((evaluation) => { + const hdfResult: ExecJSON.ControlResult = { + code_desc: this.getCodeDesc(evaluation), + start_time: evaluation.ConfigRuleInvokedTime?.toISOString() || '', + run_time: this.getRunTime(evaluation), + status: this.getStatus(evaluation), + message: this.getMessage( + evaluation, + this.getCodeDesc(evaluation), + this.getStatus(evaluation) + ) + }; + result.push(hdfResult); + const currentDate: string = new Date().toISOString(); + if (result.length === 0) { + switch ( + complianceResults.find( + (complianceResult) => + complianceResult.ConfigRuleName === configRule.ConfigRuleName + )?.Compliance?.ComplianceType + ) { + case 'NOT_APPLICABLE': + return [ + { + run_time: 0, + code_desc: NOT_APPLICABLE_MSG, + skip_message: NOT_APPLICABLE_MSG, + start_time: currentDate, + status: ExecJSON.ControlResultStatus.Skipped + } + ]; + case 'INSUFFICIENT_DATA': + return [ + { + run_time: 0, + code_desc: INSUFFICIENT_DATA_MSG, + skip_message: INSUFFICIENT_DATA_MSG, + start_time: currentDate, + status: ExecJSON.ControlResultStatus.Skipped + } + ]; + default: + return []; + } + } else { + return ruleData.push(result); + } + }); + } + + return this.appendResourceNamesToResults( + await Promise.all(ruleData), + await this.extractResourceNamesFromIds(allRulesResolved) + ); + } + + private async appendResourceNamesToResults( + completedControlResults: ExecJSON.ControlResult[][], + extractedResourceNames: Record + ) { + return completedControlResults.map((completedControlResult) => + completedControlResult.map((completedControl) => { + for (const extractedResourceName in extractedResourceNames) { + if ( + completedControl.code_desc.indexOf( + JSON.stringify(extractedResourceName) + .replace(/\"/gi, '') + .replace(/{/gi, '') + .replace(/}/gi, '') + ) !== -1 + ) { + return { + ...completedControl, + code_desc: `${completedControl.code_desc}, resource_name: ${extractedResourceNames[extractedResourceName]}` + }; + } + } + return completedControl; + }) + ); + } + + private async extractResourceNamesFromIds( + evaluationResults: AWS.ConfigService.EvaluationResults + ) { + // Map of resource types to resource IDs {resourceType: ResourceId[]} + const resourceMap: Record = {}; + // Map of resource IDs to resource names + const resolvedResourcesMap: Record = {}; + // Extract resource Ids + evaluationResults.forEach((result) => { + const resourceType: string = _.get( + result, + 'EvaluationResultIdentifier.EvaluationResultQualifier.ResourceType' + ); + const resourceId: string = _.get( + result, + 'EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId' + ); + if (!(resourceType in resourceMap)) { + resourceMap[resourceType] = [resourceId]; + } else { + if ( + !resourceMap[resourceType].includes(resourceId) && + typeof resourceId === 'string' + ) { + resourceMap[resourceType].push(resourceId); + } + } + }); + // Resolve resource names from AWS + for (const resourceType in resourceMap) { + const resourceIDSlices = this.chunkArray(resourceMap[resourceType], 20); + for (const slice of resourceIDSlices) { + await this.delay(150); + const resources = await this.configService + .listDiscoveredResources({ + resourceType: resourceType, + resourceIds: slice + }) + .promise(); + resources.resourceIdentifiers?.forEach((resource) => { + if (resource.resourceId && resource.resourceName) { + resolvedResourcesMap[resource.resourceId] = resource.resourceName; + } + }); + } + } + return resolvedResourcesMap; + } + + private getCodeDesc(result: EvaluationResult): string { + let output = ''; + if ( + result.EvaluationResultIdentifier !== undefined && + result.EvaluationResultIdentifier.EvaluationResultQualifier !== undefined + ) { + output = JSON.stringify( + result.EvaluationResultIdentifier.EvaluationResultQualifier + ) + .replace(/\"/gi, '') + .replace(/{/gi, '') + .replace(/}/gi, ''); + } + return output; + } + + private getRunTime(result: EvaluationResult): number { + let diff = 0; + if ( + result.ResultRecordedTime !== undefined && + result.ConfigRuleInvokedTime !== undefined + ) { + diff = + (result.ResultRecordedTime.getTime() - + result.ConfigRuleInvokedTime.getTime()) / + 1000; + } + return diff; + } + + private getStatus(result: EvaluationResult): ExecJSON.ControlResultStatus { + if (result.ComplianceType === 'COMPLIANT') { + return ExecJSON.ControlResultStatus.Passed; + } else if (result.ComplianceType === 'NON_COMPLIANT') { + return ExecJSON.ControlResultStatus.Failed; + } else { + return ExecJSON.ControlResultStatus.Skipped; + } + } + + private getMessage( + result: EvaluationResult, + codeDesc: string, + status: ExecJSON.ControlResultStatus + ): string | undefined { + if (status === ExecJSON.ControlResultStatus.Failed) { + return `${codeDesc}: ${ + result.Annotation || 'Rule does not pass rule compliance' + }`; + } else { + return undefined; + } + } + + private async fetchAllComplianceInfo( + configRules: ConfigRule[] + ): Promise { + const complianceResults: ComplianceByConfigRule[] = []; + // Should slice config rules into arrays of max size: 25 and make one request for each slice + const configRuleSlices = this.chunkArray(configRules, 25); + for (const slice of configRuleSlices) { + await this.delay(150); + const response = await this.configService + .describeComplianceByConfigRule({ + ConfigRuleNames: slice.map((rule) => rule.ConfigRuleName || '') + }) + .promise(); + if (response.ComplianceByConfigRules === undefined) { + throw new Error('No compliance data was returned'); + } else { + response.ComplianceByConfigRules?.forEach((compliance) => + complianceResults.push(compliance) + ); + } + } + return complianceResults; + } + + // eslint-disable-next-line @typescript-eslint/ban-types + private hdfTags(configRule: ConfigRule): Record { + let result = {}; + const sourceIdentifier = configRule.Source?.SourceIdentifier; + result = _.set(result, 'nist', []); + let defaultMatch: string[] | null = []; + if (sourceIdentifier !== undefined) { + defaultMatch = AWS_CONFIG_MAPPING.nistFilter([sourceIdentifier]); + } + if (Array.isArray(defaultMatch) && defaultMatch.length !== 0) { + result = _.set( + result, + 'nist', + _.get(result, 'nist').concat(defaultMatch) + ); + } + if ( + Array.isArray(_.get(result, 'nist')) && + _.get(result, 'nist').length === 0 + ) { + result = _.set(result, 'nist', ['unmapped']); + } + return result; + } + + private checkText(configRule: ConfigRule): string { + let params: any[] = []; + if ( + configRule.InputParameters !== undefined && + configRule.InputParameters !== '{}' + ) { + params = configRule.InputParameters.replace(/{/gi, '') + .replace(/}/gi, '') + .split(','); + } + const checkText = []; + checkText.push(`ARN: ${configRule.ConfigRuleArn || 'N/A'}`); + checkText.push( + `Source Identifier: ${configRule.Source?.SourceIdentifier || 'N/A'}` + ); + if (params.length !== 0) { + checkText.push(`${params.join('
').replace(/\"/gi, '')}`); + } + return checkText.join('
'); + } + + private hdfDescriptions(configRule: ConfigRule) { + return [ + { + data: this.checkText(configRule), + label: 'check' + } + ]; + } + + private getAccountId(arn: string): string { + const matches = arn.match(/:(\d{12}):config-rule/); + if (matches === null) { + return 'no-account-id'; + } else { + return matches[0]; + } + } + + private async getControls(): Promise { + let index = 0; + return (await this.issues).map((issue: ConfigRule) => { + const control: ExecJSON.Control = { + id: issue.ConfigRuleId || '', + title: `${this.getAccountId(issue.ConfigRuleArn || '')} - ${ + issue.ConfigRuleName + }` + .replace(/:/gi, '') + .replace(/config-rule/gi, ''), + desc: issue.Description || null, + impact: this.getImpact(issue), + tags: this.hdfTags(issue), + descriptions: this.hdfDescriptions(issue), + refs: [], + source_location: {ref: issue.ConfigRuleArn, line: 1}, + code: '', + results: this.results[index] + }; + index++; + return control; + }); + } + + private getImpact(issue: ConfigRule): number { + if (_.get(issue, 'compliance') === 'NOT_APPLICABLE') { + return 0; + } else { + return 0.5; + } + } + + public async toHdf(): Promise { + const hdf: ExecJSON.Execution = { + platform: { + name: 'Heimdall Tools', + release: HeimdallToolsVersion, + target_id: '' + }, + version: HeimdallToolsVersion, + statistics: { + //aws_config_sdk_version: ConfigService., // How do i get the sdk version? + duration: null + }, + profiles: [ + { + name: NAME, + version: '', + title: NAME, + maintainer: null, + summary: NAME, + license: null, + copyright: null, + copyright_email: null, + supports: [], + attributes: [], + depends: [], + groups: [], + status: 'loaded', + controls: await this.getControls(), + sha256: '' + } + ] + }; + return hdf; + } +} diff --git a/libs/hdf-converters/test/hdf_checker.spec.ts b/libs/hdf-converters/test/hdf_checker.spec.ts index 1b6326b12a..b0cbe542a2 100644 --- a/libs/hdf-converters/test/hdf_checker.spec.ts +++ b/libs/hdf-converters/test/hdf_checker.spec.ts @@ -67,7 +67,7 @@ test('Test converter toASFF function', () => { expect(omitASFFTimes(converted)).toEqual(omitASFFTimes(expectedJSON)); }); -describe('asff_mapper', () => { +describe('Test asff_mapper', () => { it('Successfully converts Native ASFF', () => { const mapper = new ASFFMapper( fs.readFileSync( @@ -125,7 +125,7 @@ describe('asff_mapper', () => { }); }); -test('Test burpsuite_mapper', () => { +test('burpsuite_mapper', () => { const mapper = new BurpSuiteMapper( fs.readFileSync( 'sample_jsons/burpsuite_mapper/sample_input_report/zero.webappsecurity.com.min', @@ -143,7 +143,7 @@ test('Test burpsuite_mapper', () => { ); }); -test('Test jfrog_xray_mapper', () => { +test('jfrog_xray_mapper', () => { const mapper = new JfrogXrayMapper( fs.readFileSync( 'sample_jsons/jfrog_xray_mapper/sample_input_report/jfrog_xray_sample.json', @@ -160,7 +160,8 @@ test('Test jfrog_xray_mapper', () => { ) ); }); -test('Test nikto_mapper', () => { + +test('nikto_mapper', () => { const mapper = new NiktoMapper( fs.readFileSync( 'sample_jsons/nikto_mapper/sample_input_report/zero.webappsecurity.json', @@ -177,7 +178,8 @@ test('Test nikto_mapper', () => { ) ); }); -test('Test sarif_mapper', () => { + +test('sarif_mapper', () => { const mapper = new SarifMapper( fs.readFileSync( 'sample_jsons/sarif_mapper/sample_input_report/sarif_input.sarif', @@ -194,7 +196,7 @@ test('Test sarif_mapper', () => { ) ); }); -test('Test scoutsuite_mapper', () => { +test('scoutsuite_mapper', () => { const mapper = new ScoutsuiteMapper( fs.readFileSync( 'sample_jsons/scoutsuite_mapper/sample_input_report/scoutsuite_sample.js', @@ -213,7 +215,7 @@ test('Test scoutsuite_mapper', () => { ) ); }); -test('Test sonarqube_mapper', async () => { +test('sonarqube_mapper', async () => { const mapper = new SonarQubeResults( 'http://127.0.0.1:3001', 'xss', @@ -230,8 +232,7 @@ test('Test sonarqube_mapper', async () => { ) ); }); - -test('Test xccdf_results_mapper', () => { +test('xccdf_results_mapper', () => { const mapper = new XCCDFResultsMapper( fs.readFileSync( 'sample_jsons/xccdf_results_mapper/sample_input_report/xccdf-results.xml', @@ -248,7 +249,7 @@ test('Test xccdf_results_mapper', () => { ) ); }); -test('Test zap_mapper webgoat.json', () => { +test('zap_mapper webgoat.json', () => { const mapper = new ZapMapper( fs.readFileSync( 'sample_jsons/zap_mapper/sample_input_report/webgoat.json', @@ -266,7 +267,7 @@ test('Test zap_mapper webgoat.json', () => { ) ); }); -test('Test zap_mapper zero.webappsecurity.json', () => { +test('zap_mapper zero.webappsecurity.json', () => { const mapper = new ZapMapper( fs.readFileSync( 'sample_jsons/zap_mapper/sample_input_report/zero.webappsecurity.json', diff --git a/yarn.lock b/yarn.lock index 4df787bd52..dd4c887e16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5855,7 +5855,7 @@ autoprefixer@^9.8.6: postcss "^7.0.32" postcss-value-parser "^4.1.0" -aws-sdk@^2.573.0: +aws-sdk@^2.1046.0, aws-sdk@^2.573.0: version "2.1046.0" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1046.0.tgz#9147b0fa1c86acbebd1a061e951ab5012f4499d7" integrity sha512-ocwHclMXdIA+NWocUyvp9Ild3/zy2vr5mHp3mTyodf0WU5lzBE8PocCVLSWhMAXLxyia83xv2y5f5AzAcetbqA==