heimdall_tools 1.3.45 → 1.3.49
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +236 -176
- data/lib/data/scoutsuite-nist-mapping.csv +140 -0
- data/lib/heimdall_tools/asff_compatible_products/firewall_manager.rb +11 -0
- data/lib/heimdall_tools/asff_compatible_products/prowler.rb +19 -0
- data/lib/heimdall_tools/asff_compatible_products/securityhub.rb +89 -0
- data/lib/heimdall_tools/asff_mapper.rb +232 -0
- data/lib/heimdall_tools/aws_config_mapper.rb +5 -5
- data/lib/heimdall_tools/cli.rb +61 -6
- data/lib/heimdall_tools/fortify_mapper.rb +3 -3
- data/lib/heimdall_tools/help/asff_mapper.md +6 -0
- data/lib/heimdall_tools/help/prowler_mapper.md +5 -0
- data/lib/heimdall_tools/help/sarif_mapper.md +12 -0
- data/lib/heimdall_tools/help/scoutsuite_mapper.md +7 -0
- data/lib/heimdall_tools/nessus_mapper.rb +14 -6
- data/lib/heimdall_tools/prowler_mapper.rb +8 -0
- data/lib/heimdall_tools/sarif_mapper.rb +198 -0
- data/lib/heimdall_tools/scoutsuite_mapper.rb +180 -0
- data/lib/heimdall_tools/sonarqube_mapper.rb +5 -1
- data/lib/heimdall_tools/xccdf_results_mapper.rb +161 -0
- data/lib/heimdall_tools/zap_mapper.rb +0 -2
- data/lib/heimdall_tools.rb +5 -0
- metadata +46 -4
@@ -0,0 +1,140 @@
|
|
1
|
+
rule,nistid
|
2
|
+
acm-certificate-with-close-expiration-date,SC-12
|
3
|
+
acm-certificate-with-transparency-logging-disabled,SC-12
|
4
|
+
cloudformation-stack-with-role,AC-6
|
5
|
+
cloudtrail-duplicated-global-services-logging,AU-6
|
6
|
+
cloudtrail-no-cloudwatch-integration,AU-12|SI-4(2)
|
7
|
+
cloudtrail-no-data-logging,AU-12
|
8
|
+
cloudtrail-no-encryption-with-kms,AU-6
|
9
|
+
cloudtrail-no-global-services-logging,AU-12
|
10
|
+
cloudtrail-no-log-file-validation,AU-6
|
11
|
+
cloudtrail-no-logging,AU-12
|
12
|
+
cloudtrail-not-configured,AU-12
|
13
|
+
cloudwatch-alarm-without-actions,AU-12
|
14
|
+
config-recorder-not-configured,CM-8|CM-8(2)|CM-8(6)
|
15
|
+
ec2-ami-public,AC-3
|
16
|
+
ec2-default-security-group-in-use,AC-3(3)
|
17
|
+
ec2-default-security-group-with-rules,AC-3(3)
|
18
|
+
ec2-ebs-snapshot-not-encrypted,SC-28
|
19
|
+
ec2-ebs-snapshot-public,AC-3
|
20
|
+
ec2-ebs-volume-not-encrypted,SC-28
|
21
|
+
ec2-instance-in-security-group,CM-7(1)
|
22
|
+
ec2-instance-type,CM-2
|
23
|
+
ec2-instance-types,CM-2
|
24
|
+
ec2-instance-with-public-ip,AC-3
|
25
|
+
ec2-instance-with-user-data-secrets,AC-3
|
26
|
+
ec2-security-group-opens-all-ports,CM-7(1)
|
27
|
+
ec2-security-group-opens-all-ports-to-all,CM-7(1)
|
28
|
+
ec2-security-group-opens-all-ports-to-self,CM-7(1)
|
29
|
+
ec2-security-group-opens-icmp-to-all,CM-7(1)
|
30
|
+
ec2-security-group-opens-known-port-to-all,CM-7(1)
|
31
|
+
ec2-security-group-opens-plaintext-port,CM-7(1)
|
32
|
+
ec2-security-group-opens-port-range,CM-7(1)
|
33
|
+
ec2-security-group-opens-port-to-all,CM-7(1)
|
34
|
+
ec2-security-group-whitelists-aws,CM-7(1)
|
35
|
+
ec2-security-group-whitelists-aws-ip-from-banned-region,CM-7(1)
|
36
|
+
ec2-security-group-whitelists-non-elastic-ips,CM-7(1)
|
37
|
+
ec2-security-group-whitelists-unknown-aws,CM-7(1)
|
38
|
+
ec2-security-group-whitelists-unknown-cidrs,CM-7(1)
|
39
|
+
ec2-unused-security-group,CM-7(1)
|
40
|
+
elb-listener-allowing-cleartext,SC-8
|
41
|
+
elb-no-access-logs,AU-12
|
42
|
+
elb-older-ssl-policy,SC-8
|
43
|
+
elbv2-http-request-smuggling,SC-8
|
44
|
+
elbv2-listener-allowing-cleartext,SC-8
|
45
|
+
elbv2-no-access-logs,AU-12
|
46
|
+
elbv2-no-deletion-protection,SI-7
|
47
|
+
elbv2-older-ssl-policy,SC-8
|
48
|
+
iam-assume-role-lacks-external-id-and-mfa,AC-17
|
49
|
+
iam-assume-role-no-mfa,AC-6
|
50
|
+
iam-assume-role-policy-allows-all,AC-6
|
51
|
+
iam-ec2-role-without-instances,AC-6
|
52
|
+
iam-group-with-inline-policies,AC-6
|
53
|
+
iam-group-with-no-users,AC-6
|
54
|
+
iam-human-user-with-policies,AC-6
|
55
|
+
iam-inline-policy-allows-non-sts-action,AC-6
|
56
|
+
iam-inline-policy-allows-NotActions,AC-6
|
57
|
+
iam-inline-policy-for-role,AC-6
|
58
|
+
iam-managed-policy-allows-full-privileges,AC-6
|
59
|
+
iam-managed-policy-allows-non-sts-action,AC-6
|
60
|
+
iam-managed-policy-allows-NotActions,AC-6
|
61
|
+
iam-managed-policy-for-role,AC-6
|
62
|
+
iam-managed-policy-no-attachments,AC-6
|
63
|
+
iam-no-support-role,IR-7
|
64
|
+
iam-password-policy-expiration-threshold,AC-2
|
65
|
+
iam-password-policy-minimum-length,AC-2
|
66
|
+
iam-password-policy-no-expiration,AC-2
|
67
|
+
iam-password-policy-no-lowercase-required,AC-2
|
68
|
+
iam-password-policy-no-number-required,AC-2
|
69
|
+
iam-password-policy-no-symbol-required,AC-2
|
70
|
+
iam-password-policy-no-uppercase-required,AC-2
|
71
|
+
iam-password-policy-reuse-enabled,IA-5(1)
|
72
|
+
iam-role-with-inline-policies,AC-6
|
73
|
+
iam-root-account-no-hardware-mfa,IA-2(1)
|
74
|
+
iam-root-account-no-mfa,IA-2(1)
|
75
|
+
iam-root-account-used-recently,AC-6(9)
|
76
|
+
iam-root-account-with-active-certs,AC-6(9)
|
77
|
+
iam-root-account-with-active-keys,AC-6(9)
|
78
|
+
iam-service-user-with-password,AC-2
|
79
|
+
iam-unused-credentials-not-disabled,AC-2
|
80
|
+
iam-user-no-key-rotation,AC-2
|
81
|
+
iam-user-not-in-category-group,AC-2
|
82
|
+
iam-user-not-in-common-group,AC-2
|
83
|
+
iam-user-unused-access-key-initial-setup,AC-2
|
84
|
+
iam-user-with-multiple-access-keys,IA-2
|
85
|
+
iam-user-without-mfa,IA-2(1)
|
86
|
+
iam-user-with-password-and-key,IA-2
|
87
|
+
iam-user-with-policies,AC-2
|
88
|
+
kms-cmk-rotation-disabled,SC-12
|
89
|
+
logs-no-alarm-aws-configuration-changes,CM-8|CM-8(2)|CM-8(6)
|
90
|
+
logs-no-alarm-cloudtrail-configuration-changes,AU-6
|
91
|
+
logs-no-alarm-cmk-deletion,AC-2
|
92
|
+
logs-no-alarm-console-authentication-failures,AC-2
|
93
|
+
logs-no-alarm-iam-policy-changes,AC-2
|
94
|
+
logs-no-alarm-nacl-changes,CM-6(2)
|
95
|
+
logs-no-alarm-network-gateways-changes,AU-12|CM-6(2)
|
96
|
+
logs-no-alarm-root-usage,AU-2
|
97
|
+
logs-no-alarm-route-table-changes,AU-12|CM-6(2)
|
98
|
+
logs-no-alarm-s3-policy-changes,AC-6|AU-12
|
99
|
+
logs-no-alarm-security-group-changes,AC-2(4)
|
100
|
+
logs-no-alarm-signin-without-mfa,AC-2
|
101
|
+
logs-no-alarm-unauthorized-api-calls,AU-6|SI-4(2)
|
102
|
+
logs-no-alarm-vpc-changes,CM-6(1)
|
103
|
+
rds-instance-backup-disabled,CP-9
|
104
|
+
rds-instance-ca-certificate-deprecated,SC-12
|
105
|
+
rds-instance-no-minor-upgrade,SI-2
|
106
|
+
rds-instance-short-backup-retention-period,CP-9
|
107
|
+
rds-instance-single-az,CP-7
|
108
|
+
rds-instance-storage-not-encrypted,SC-28
|
109
|
+
rds-postgres-instance-with-invalid-certificate,SC-12
|
110
|
+
rds-security-group-allows-all,CM-7(1)
|
111
|
+
rds-snapshot-public,SC-28
|
112
|
+
redshift-cluster-database-not-encrypted,SC-28
|
113
|
+
redshift-cluster-no-version-upgrade,SI-2
|
114
|
+
redshift-cluster-publicly-accessible,AC-3
|
115
|
+
redshift-parameter-group-logging-disabled,AU-12
|
116
|
+
redshift-parameter-group-ssl-not-required,SC-8
|
117
|
+
redshift-security-group-whitelists-all,CM-7(1)
|
118
|
+
route53-domain-no-autorenew,SC-2
|
119
|
+
route53-domain-no-transferlock,SC-2
|
120
|
+
route53-domain-transferlock-not-authorized,SC-2
|
121
|
+
s3-bucket-allowing-cleartext,SC-28
|
122
|
+
s3-bucket-no-default-encryption,SC-28
|
123
|
+
s3-bucket-no-logging,AU-2|AU-12
|
124
|
+
s3-bucket-no-mfa-delete,SI-7
|
125
|
+
s3-bucket-no-versioning,SI-7
|
126
|
+
s3-bucket-world-acl,AC-3(3)
|
127
|
+
s3-bucket-world-policy-arg,AC-3(3)
|
128
|
+
s3-bucket-world-policy-star,AC-3(3)
|
129
|
+
ses-identity-dkim-not-enabled,SC-23
|
130
|
+
ses-identity-dkim-not-verified,SC-23
|
131
|
+
ses-identity-world-policy,AC-6
|
132
|
+
sns-topic-world-policy,AC-6
|
133
|
+
sqs-queue-world-policy,AC-6
|
134
|
+
vpc-custom-network-acls-allow-all,SC-7
|
135
|
+
vpc-default-network-acls-allow-all,SC-7
|
136
|
+
vpc-network-acl-not-used,SC-7
|
137
|
+
vpc-routing-tables-with-peering,AC-3(3)
|
138
|
+
vpc-subnet-with-bad-acls,SC-7
|
139
|
+
vpc-subnet-with-default-acls,SC-7
|
140
|
+
vpc-subnet-without-flow-log,AU-12
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module HeimdallTools
|
2
|
+
class FirewallManager
|
3
|
+
def self.finding_id(finding, *, encode:, **)
|
4
|
+
encode.call(finding['Title'])
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.product_name(findings, *, encode:, **)
|
8
|
+
encode.call("#{findings[0]['ProductFields']['aws/securityhub/CompanyName']} #{findings[0]['ProductFields']['aws/securityhub/ProductName']}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module HeimdallTools
|
2
|
+
class Prowler
|
3
|
+
def self.subfindings_code_desc(finding, *, encode:, **)
|
4
|
+
encode.call(finding['Description'])
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.finding_id(finding, *, encode:, **)
|
8
|
+
encode.call(finding['GeneratorId'].partition('-')[-1])
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.product_name(findings, *, encode:, **)
|
12
|
+
encode.call(findings[0]['ProductFields']['ProviderName'])
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.desc(*, **)
|
16
|
+
' '
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module HeimdallTools
|
5
|
+
class SecurityHub
|
6
|
+
private_class_method def self.corresponding_control(controls, finding)
|
7
|
+
controls.find { |c| c['StandardsControlArn'] == finding['ProductFields']['StandardsControlArn'] }
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.supporting_docs(standards:)
|
11
|
+
begin
|
12
|
+
controls = standards.nil? ? nil : standards.map { |s| JSON.parse(s)['Controls'] }.flatten
|
13
|
+
rescue StandardError => e
|
14
|
+
raise "Invalid supporting docs for Security Hub:\nException: #{e}"
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
resource_dir = Pathname.new(__FILE__).join('../../../data')
|
19
|
+
aws_config_mapping_file = File.join(resource_dir, 'aws-config-mapping.csv')
|
20
|
+
aws_config_mapping = CSV.read(aws_config_mapping_file, { encoding: 'UTF-8', headers: true, header_converters: :symbol }).map(&:to_hash)
|
21
|
+
rescue StandardError => e
|
22
|
+
raise "Invalid AWS Config mapping file:\nException: #{e}"
|
23
|
+
end
|
24
|
+
|
25
|
+
{ controls: controls, aws_config_mapping: aws_config_mapping }
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.finding_id(finding, *, encode:, controls: nil, **)
|
29
|
+
ret = if !controls.nil? && !(control = corresponding_control(controls, finding)).nil?
|
30
|
+
control['ControlId']
|
31
|
+
elsif finding['ProductFields'].member?('ControlId') # check if aws
|
32
|
+
finding['ProductFields']['ControlId']
|
33
|
+
elsif finding['ProductFields'].member?('RuleId') # check if cis
|
34
|
+
finding['ProductFields']['RuleId']
|
35
|
+
else
|
36
|
+
finding['GeneratorId'].split('/')[-1]
|
37
|
+
end
|
38
|
+
encode.call(ret)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.finding_impact(finding, *, controls: nil, **)
|
42
|
+
if !controls.nil? && !(control = corresponding_control(controls, finding)).nil?
|
43
|
+
imp = control['SeverityRating'].to_sym
|
44
|
+
else
|
45
|
+
# severity is required, but can be either 'label' or 'normalized' internally with 'label' being preferred. other values can be in here too such as the original severity rating.
|
46
|
+
imp = finding['Severity'].key?('Label') ? finding['Severity']['Label'].to_sym : finding['Severity']['Normalized']/100.0
|
47
|
+
# securityhub asff file does not contain accurate severity information by setting things that shouldn't be informational to informational: when additional context, i.e. standards, is not provided, set informational to medium.
|
48
|
+
imp = :MEDIUM if imp.is_a?(Symbol) && imp == :INFORMATIONAL
|
49
|
+
end
|
50
|
+
imp
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.finding_nist_tag(finding, *, aws_config_mapping:, **)
|
54
|
+
return {} unless finding['ProductFields']['RelatedAWSResources:0/type'] == 'AWS::Config::ConfigRule'
|
55
|
+
|
56
|
+
entries = aws_config_mapping.select { |rule| finding['ProductFields']['RelatedAWSResources:0/name'].include? rule[:awsconfigrulename] }
|
57
|
+
entries.map do |rule|
|
58
|
+
tags_joined = rule[:nistid].split('|') # subheadings are joined together in the csv file
|
59
|
+
tags_joined.map do |tag|
|
60
|
+
if (i = tag.index('(')).nil?
|
61
|
+
tag
|
62
|
+
else
|
63
|
+
tag[i..-1].scan(/\(.+?\)/).map { |subheading| "#{tag[0..i-1]}#{subheading}" }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end.flatten.uniq
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.finding_title(finding, *, encode:, controls: nil, **)
|
70
|
+
ret = if !controls.nil? && !(control = corresponding_control(controls, finding)).nil?
|
71
|
+
control['Title']
|
72
|
+
else
|
73
|
+
finding['Title']
|
74
|
+
end
|
75
|
+
encode.call(ret)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.product_name(findings, *, encode:, **)
|
79
|
+
# "#{findings[0]['ProductFields']['aws/securityhub/CompanyName']} #{findings[0]['ProductFields']['aws/securityhub/ProductName']}"
|
80
|
+
# not using above due to wanting to provide the standard's name instead
|
81
|
+
if findings[0]['Types'][0].split('/')[-1].gsub(/-/, ' ').downcase == findings[0]['ProductFields']['StandardsControlArn'].split('/')[-4].gsub(/-/, ' ').downcase
|
82
|
+
standardname = findings[0]['Types'][0].split('/')[-1].gsub(/-/, ' ')
|
83
|
+
else
|
84
|
+
standardname = findings[0]['ProductFields']['StandardsControlArn'].split('/')[-4].gsub(/-/, ' ').split.map(&:capitalize).join(' ')
|
85
|
+
end
|
86
|
+
encode.call("#{standardname} v#{findings[0]['ProductFields']['StandardsControlArn'].split('/')[-2]}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
require 'htmlentities'
|
5
|
+
|
6
|
+
require 'heimdall_tools/hdf'
|
7
|
+
require 'heimdall_tools/asff_compatible_products/firewall_manager'
|
8
|
+
require 'heimdall_tools/asff_compatible_products/prowler'
|
9
|
+
require 'heimdall_tools/asff_compatible_products/securityhub'
|
10
|
+
|
11
|
+
module HeimdallTools
|
12
|
+
DEFAULT_NIST_TAG = %w{SA-11 RA-5}.freeze
|
13
|
+
|
14
|
+
INSPEC_INPUTS_MAPPING = {
|
15
|
+
string: 'String',
|
16
|
+
numeric: 'Numeric',
|
17
|
+
regexp: 'Regexp',
|
18
|
+
array: 'Array',
|
19
|
+
hash: 'Hash',
|
20
|
+
boolean: 'Boolean',
|
21
|
+
any: 'Any'
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
# Loading spinner sign
|
25
|
+
$spinner = Enumerator.new do |e|
|
26
|
+
loop do
|
27
|
+
e.yield '|'
|
28
|
+
e.yield '/'
|
29
|
+
e.yield '-'
|
30
|
+
e.yield '\\'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# TODO: use hash.dig and safe navigation operator throughout
|
35
|
+
class ASFFMapper
|
36
|
+
IMPACT_MAPPING = {
|
37
|
+
CRITICAL: 0.9,
|
38
|
+
HIGH: 0.7,
|
39
|
+
MEDIUM: 0.5,
|
40
|
+
LOW: 0.3,
|
41
|
+
INFORMATIONAL: 0.0
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
PRODUCT_ARN_MAPPING = {
|
45
|
+
%r{arn:.+:securityhub:.+:.*:product/aws/firewall-manager} => FirewallManager,
|
46
|
+
%r{arn:.+:securityhub:.+:.*:product/aws/securityhub} => SecurityHub,
|
47
|
+
%r{arn:.+:securityhub:.+:.*:product/prowler/prowler} => Prowler
|
48
|
+
}.freeze
|
49
|
+
|
50
|
+
def initialize(asff_json, securityhub_standards_json_array: nil, meta: nil)
|
51
|
+
@meta = meta
|
52
|
+
|
53
|
+
@supporting_docs = {}
|
54
|
+
@supporting_docs[SecurityHub] = SecurityHub.supporting_docs({ standards: securityhub_standards_json_array })
|
55
|
+
|
56
|
+
begin
|
57
|
+
asff_required_keys = %w{AwsAccountId CreatedAt Description GeneratorId Id ProductArn Resources SchemaVersion Severity Title Types UpdatedAt}
|
58
|
+
@report = JSON.parse(asff_json)
|
59
|
+
if @report.length == 1 && @report.member?('Findings') && @report['Findings'].each { |finding| asff_required_keys.to_set.difference(finding.keys.to_set).none? }.all?
|
60
|
+
# ideal case that is spec compliant
|
61
|
+
# might need to ensure that the file is utf-8 encoded and remove a BOM if one exists
|
62
|
+
elsif asff_required_keys.to_set.difference(@report.keys.to_set).none?
|
63
|
+
# individual finding so have to add wrapping array
|
64
|
+
@report = { 'Findings' => [@report] }
|
65
|
+
else
|
66
|
+
raise 'Not a findings file nor an individual finding'
|
67
|
+
end
|
68
|
+
rescue StandardError => e
|
69
|
+
raise "Invalid ASFF file provided:\nException: #{e}"
|
70
|
+
end
|
71
|
+
|
72
|
+
@coder = HTMLEntities.new
|
73
|
+
end
|
74
|
+
|
75
|
+
def encode(string)
|
76
|
+
@coder.encode(string, :basic, :named, :decimal)
|
77
|
+
end
|
78
|
+
|
79
|
+
def external_product_handler(product, data, func, default)
|
80
|
+
if (product.is_a?(Regexp) || (arn = PRODUCT_ARN_MAPPING.keys.find { |a| product.match(a) })) && PRODUCT_ARN_MAPPING.key?(arn || product) && PRODUCT_ARN_MAPPING[arn || product].respond_to?(func)
|
81
|
+
keywords = { encode: method(:encode) }
|
82
|
+
keywords = keywords.merge(@supporting_docs[PRODUCT_ARN_MAPPING[arn || product]]) if @supporting_docs.member?(PRODUCT_ARN_MAPPING[arn || product])
|
83
|
+
PRODUCT_ARN_MAPPING[arn || product].send(func, data, **keywords)
|
84
|
+
elsif default.is_a? Proc
|
85
|
+
default.call
|
86
|
+
else
|
87
|
+
default
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def nist_tag(finding)
|
92
|
+
tags = external_product_handler(finding['ProductArn'], finding, :finding_nist_tag, {})
|
93
|
+
tags.empty? ? DEFAULT_NIST_TAG : tags
|
94
|
+
end
|
95
|
+
|
96
|
+
def impact(finding)
|
97
|
+
# there can be findings listed that are intentionally ignored due to the underlying control being superceded by a control from a different standard
|
98
|
+
if finding.member?('Workflow') && finding['Workflow'].member?('Status') && finding['Workflow']['Status'] == 'SUPPRESSED'
|
99
|
+
imp = :INFORMATIONAL
|
100
|
+
else
|
101
|
+
# severity is required, but can be either 'label' or 'normalized' internally with 'label' being preferred. other values can be in here too such as the original severity rating.
|
102
|
+
default = proc { finding['Severity'].key?('Label') ? finding['Severity']['Label'].to_sym : finding['Severity']['Normalized']/100.0 }
|
103
|
+
imp = external_product_handler(finding['ProductArn'], finding, :finding_impact, default)
|
104
|
+
end
|
105
|
+
imp.is_a?(Symbol) ? IMPACT_MAPPING[imp] : imp
|
106
|
+
end
|
107
|
+
|
108
|
+
def desc_tags(data, label)
|
109
|
+
{ data: data || NA_STRING, label: label || NA_STRING }
|
110
|
+
end
|
111
|
+
|
112
|
+
def subfindings(finding)
|
113
|
+
subfinding = {}
|
114
|
+
|
115
|
+
statusreason = finding['Compliance']['StatusReasons'].map { |reason| reason.flatten.map { |string| encode(string) } }.flatten.join("\n") if finding.key?('Compliance') && finding['Compliance'].key?('StatusReasons')
|
116
|
+
if finding.key?('Compliance') && finding['Compliance'].key?('Status')
|
117
|
+
case finding['Compliance']['Status']
|
118
|
+
when 'PASSED'
|
119
|
+
subfinding['status'] = 'passed'
|
120
|
+
subfinding['message'] = statusreason if statusreason
|
121
|
+
when 'WARNING'
|
122
|
+
subfinding['status'] = 'skipped'
|
123
|
+
subfinding['skip_message'] = statusreason if statusreason
|
124
|
+
when 'FAILED'
|
125
|
+
subfinding['status'] = 'failed'
|
126
|
+
subfinding['message'] = statusreason if statusreason
|
127
|
+
when 'NOT_AVAILABLE'
|
128
|
+
# primary meaning is that the check could not be performed due to a service outage or API error, but it's also overloaded to mean NOT_APPLICABLE so technically 'skipped' or 'error' could be applicable, but AWS seems to do the equivalent of skipped
|
129
|
+
subfinding['status'] = 'skipped'
|
130
|
+
subfinding['skip_message'] = statusreason if statusreason
|
131
|
+
else
|
132
|
+
subfinding['status'] = 'error' # not a valid value for the status enum
|
133
|
+
subfinding['message'] = statusreason if statusreason
|
134
|
+
end
|
135
|
+
else
|
136
|
+
subfinding['status'] = 'skipped' # if no compliance status is provided which is a weird but possible case, then skip
|
137
|
+
subfinding['skip_message'] = statusreason if statusreason
|
138
|
+
end
|
139
|
+
|
140
|
+
subfinding['code_desc'] = external_product_handler(finding['ProductArn'], finding, :subfindings_code_desc, '')
|
141
|
+
subfinding['code_desc'] += '; ' unless subfinding['code_desc'].empty?
|
142
|
+
subfinding['code_desc'] += "Resources: [#{finding['Resources'].map { |r| "Type: #{encode(r['Type'])}, Id: #{encode(r['Id'])}#{", Partition: #{encode(r['Partition'])}" if r.key?('Partition')}#{", Region: #{encode(r['Region'])}" if r.key?('Region')}" }.join(', ')}]"
|
143
|
+
|
144
|
+
subfinding['start_time'] = finding.key?('LastObservedAt') ? finding['LastObservedAt'] : finding['UpdatedAt']
|
145
|
+
|
146
|
+
[subfinding]
|
147
|
+
end
|
148
|
+
|
149
|
+
def to_hdf
|
150
|
+
product_groups = {}
|
151
|
+
@report['Findings'].each do |finding|
|
152
|
+
printf("\rProcessing: %s", $spinner.next)
|
153
|
+
|
154
|
+
external = method(:external_product_handler).curry(4)[finding['ProductArn']][finding]
|
155
|
+
|
156
|
+
# group subfindings by asff productarn and then hdf id
|
157
|
+
item = {}
|
158
|
+
item['id'] = external[:finding_id][encode(finding['GeneratorId'])]
|
159
|
+
|
160
|
+
item['title'] = external[:finding_title][encode(finding['Title'])]
|
161
|
+
|
162
|
+
item['tags'] = { nist: nist_tag(finding) }
|
163
|
+
|
164
|
+
item['impact'] = impact(finding)
|
165
|
+
|
166
|
+
item['desc'] = encode(finding['Description'])
|
167
|
+
|
168
|
+
item['descriptions'] = []
|
169
|
+
item['descriptions'] << desc_tags(finding['Remediation']['Recommendation'].map { |_k, v| encode(v) }.join("\n"), 'fix') if finding.key?('Remediation') && finding['Remediation'].key?('Recommendation')
|
170
|
+
|
171
|
+
item['refs'] = []
|
172
|
+
item['refs'] << { url: finding['SourceUrl'] } if finding.key?('SourceUrl')
|
173
|
+
|
174
|
+
item['source_location'] = NA_HASH
|
175
|
+
|
176
|
+
item['results'] = subfindings(finding)
|
177
|
+
|
178
|
+
arn = PRODUCT_ARN_MAPPING.keys.find { |a| finding['ProductArn'].match(a) }
|
179
|
+
if arn.nil?
|
180
|
+
product_info = finding['ProductArn'].split(':')[-1]
|
181
|
+
arn = Regexp.new "arn:.+:securityhub:.+:.*:product/#{product_info.split('/')[1]}/#{product_info.split('/')[2]}"
|
182
|
+
end
|
183
|
+
product_groups[arn] = {} if product_groups[arn].nil?
|
184
|
+
product_groups[arn][item['id']] = [] if product_groups[arn][item['id']].nil?
|
185
|
+
product_groups[arn][item['id']] << [item, finding]
|
186
|
+
end
|
187
|
+
|
188
|
+
controls = []
|
189
|
+
product_groups.each do |product, id_groups|
|
190
|
+
id_groups.each do |id, data|
|
191
|
+
printf("\rProcessing: %s", $spinner.next)
|
192
|
+
|
193
|
+
external = method(:external_product_handler).curry(4)[product]
|
194
|
+
|
195
|
+
group = data.map { |d| d[0] }
|
196
|
+
findings = data.map { |d| d[1] }
|
197
|
+
|
198
|
+
product_info = findings[0]['ProductArn'].split(':')[-1].split('/')
|
199
|
+
product_name = external[findings][:product_name][encode("#{product_info[1]}/#{product_info[2]}")]
|
200
|
+
|
201
|
+
item = {}
|
202
|
+
# add product name to id if any ids are the same across products
|
203
|
+
item['id'] = product_groups.reject { |pg| pg == product }.values.any? { |ig| ig.keys.include?(id) } ? "[#{product_name}] #{id}" : id
|
204
|
+
|
205
|
+
item['title'] = "#{product_name}: #{group.map { |d| d['title'] }.uniq.join(';')}"
|
206
|
+
|
207
|
+
item['tags'] = { nist: group.map { |d| d['tags'][:nist] }.flatten.uniq }
|
208
|
+
|
209
|
+
item['impact'] = group.map { |d| d['impact'] }.max
|
210
|
+
|
211
|
+
item['desc'] = external[group][:desc][group.map { |d| d['desc'] }.uniq.join("\n")]
|
212
|
+
|
213
|
+
item['descriptions'] = group.map { |d| d['descriptions'] }.flatten.compact.reject(&:empty?).uniq
|
214
|
+
|
215
|
+
item['refs'] = group.map { |d| d['refs'] }.flatten.compact.reject(&:empty?).uniq
|
216
|
+
|
217
|
+
item['source_location'] = NA_HASH
|
218
|
+
item['code'] = JSON.pretty_generate({ Findings: findings })
|
219
|
+
|
220
|
+
item['results'] = group.map { |d| d['results'] }.flatten.uniq
|
221
|
+
|
222
|
+
controls << item
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
results = HeimdallDataFormat.new(profile_name: @meta&.key?('name') ? @meta['name'] : 'AWS Security Finding Format',
|
227
|
+
title: @meta&.key?('title') ? @meta['title'] : 'ASFF findings',
|
228
|
+
controls: controls)
|
229
|
+
results.to_hdf
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -8,7 +8,7 @@ RESOURCE_DIR = Pathname.new(__FILE__).join('../../data')
|
|
8
8
|
AWS_CONFIG_MAPPING_FILE = File.join(RESOURCE_DIR, 'aws-config-mapping.csv')
|
9
9
|
|
10
10
|
NOT_APPLICABLE_MSG = 'No AWS resources found to evaluate complaince for this rule'.freeze
|
11
|
-
INSUFFICIENT_DATA_MSG = 'Not enough data has been
|
11
|
+
INSUFFICIENT_DATA_MSG = 'Not enough data has been collected to determine compliance yet.'.freeze
|
12
12
|
|
13
13
|
##
|
14
14
|
# HDF mapper for use with AWS Config rules.
|
@@ -57,10 +57,10 @@ module HeimdallTools
|
|
57
57
|
|
58
58
|
results = HeimdallDataFormat.new(
|
59
59
|
profile_name: 'AWS Config',
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
60
|
+
title: 'AWS Config',
|
61
|
+
summary: 'AWS Config',
|
62
|
+
controls: controls,
|
63
|
+
statistics: { aws_config_sdk_version: Aws::ConfigService::GEM_VERSION },
|
64
64
|
)
|
65
65
|
results.to_hdf
|
66
66
|
end
|
data/lib/heimdall_tools/cli.rb
CHANGED
@@ -41,6 +41,15 @@ module HeimdallTools
|
|
41
41
|
File.write(options[:output], hdf)
|
42
42
|
end
|
43
43
|
|
44
|
+
desc 'xccdf_results_mapper', 'xccdf_results_mapper translates SCAP client XCCDF-Results XML report to HDF format Json be viewed on Heimdall'
|
45
|
+
long_desc Help.text(:xccdf_results_mapper)
|
46
|
+
option :xml, required: true, aliases: '-x'
|
47
|
+
option :output, required: true, aliases: '-o'
|
48
|
+
def xccdf_results_mapper
|
49
|
+
hdf = HeimdallTools::XCCDFResultsMapper.new(File.read(options[:xml])).to_hdf
|
50
|
+
File.write(options[:output], hdf)
|
51
|
+
end
|
52
|
+
|
44
53
|
desc 'nessus_mapper', 'nessus_mapper translates nessus xml report to HDF format Json be viewed on Heimdall'
|
45
54
|
long_desc Help.text(:nessus_mapper)
|
46
55
|
option :xml, required: true, aliases: '-x'
|
@@ -61,7 +70,7 @@ module HeimdallTools
|
|
61
70
|
option :output_prefix, required: true, aliases: '-o'
|
62
71
|
def snyk_mapper
|
63
72
|
hdfs = HeimdallTools::SnykMapper.new(File.read(options[:json]), options[:name]).to_hdf
|
64
|
-
puts "\
|
73
|
+
puts "\rHDF Generated:\n"
|
65
74
|
hdfs.each_key do |host|
|
66
75
|
File.write("#{options[:output_prefix]}-#{host}.json", hdfs[host])
|
67
76
|
puts "#{options[:output_prefix]}-#{host}.json"
|
@@ -75,7 +84,7 @@ module HeimdallTools
|
|
75
84
|
def nikto_mapper
|
76
85
|
hdf = HeimdallTools::NiktoMapper.new(File.read(options[:json])).to_hdf
|
77
86
|
File.write(options[:output], hdf)
|
78
|
-
puts "\
|
87
|
+
puts "\rHDF Generated:\n"
|
79
88
|
puts options[:output].to_s
|
80
89
|
end
|
81
90
|
|
@@ -86,7 +95,7 @@ module HeimdallTools
|
|
86
95
|
def jfrog_xray_mapper
|
87
96
|
hdf = HeimdallTools::JfrogXrayMapper.new(File.read(options[:json])).to_hdf
|
88
97
|
File.write(options[:output], hdf)
|
89
|
-
puts "\
|
98
|
+
puts "\rHDF Generated:\n"
|
90
99
|
puts options[:output].to_s
|
91
100
|
end
|
92
101
|
|
@@ -97,7 +106,7 @@ module HeimdallTools
|
|
97
106
|
def dbprotect_mapper
|
98
107
|
hdf = HeimdallTools::DBProtectMapper.new(File.read(options[:xml])).to_hdf
|
99
108
|
File.write(options[:output], hdf)
|
100
|
-
puts "\
|
109
|
+
puts "\rHDF Generated:\n"
|
101
110
|
puts options[:output].to_s
|
102
111
|
end
|
103
112
|
|
@@ -108,7 +117,7 @@ module HeimdallTools
|
|
108
117
|
def aws_config_mapper
|
109
118
|
hdf = HeimdallTools::AwsConfigMapper.new(options[:custom_mapping]).to_hdf
|
110
119
|
File.write(options[:output], hdf)
|
111
|
-
puts "\
|
120
|
+
puts "\rHDF Generated:\n"
|
112
121
|
puts options[:output].to_s
|
113
122
|
end
|
114
123
|
|
@@ -119,7 +128,53 @@ module HeimdallTools
|
|
119
128
|
def netsparker_mapper
|
120
129
|
hdf = HeimdallTools::NetsparkerMapper.new(File.read(options[:xml])).to_hdf
|
121
130
|
File.write(options[:output], hdf)
|
122
|
-
puts "\
|
131
|
+
puts "\rHDF Generated:\n"
|
132
|
+
puts options[:output].to_s
|
133
|
+
end
|
134
|
+
|
135
|
+
desc 'sarif_mapper', 'sarif_mapper translates a SARIF JSON file into HDF format JSON to be viewable in Heimdall'
|
136
|
+
long_desc Help.text(:sarif_mapper)
|
137
|
+
option :json, required: true, aliases: '-j'
|
138
|
+
option :output, required: true, aliases: '-o'
|
139
|
+
option :verbose, type: :boolean, aliases: '-V'
|
140
|
+
def sarif_mapper
|
141
|
+
hdf = HeimdallTools::SarifMapper.new(File.read(options[:json])).to_hdf
|
142
|
+
File.write(options[:output], hdf)
|
143
|
+
puts "\rHDF Generated:\n"
|
144
|
+
puts options[:output].to_s
|
145
|
+
end
|
146
|
+
|
147
|
+
desc 'scoutsuite_mapper', 'scoutsuite_mapper translates Scout Suite results from Javascript to HDF-formatted JSON so as to be viewable on Heimdall'
|
148
|
+
long_desc Help.text(:scoutsuite_mapper)
|
149
|
+
option :javascript, required: true, banner: 'SCOUTSUITE-RESULTS-JS', aliases: ['-i', '--input', '-j']
|
150
|
+
option :output, required: true, banner: 'HDF-SCAN-RESULTS-JSON', aliases: '-o'
|
151
|
+
def scoutsuite_mapper
|
152
|
+
hdf = HeimdallTools::ScoutSuiteMapper.new(File.read(options[:javascript])).to_hdf
|
153
|
+
File.write(options[:output], hdf)
|
154
|
+
puts "\rHDF Generated:\n"
|
155
|
+
puts options[:output].to_s
|
156
|
+
end
|
157
|
+
|
158
|
+
desc 'asff_mapper', 'asff_mapper translates AWS Security Finding Format results from JSON to HDF-formatted JSON so as to be viewable on Heimdall'
|
159
|
+
long_desc Help.text(:asff_mapper)
|
160
|
+
option :json, required: true, banner: 'ASFF-FINDING-JSON', aliases: ['-i', '--input', '-j']
|
161
|
+
option :securityhub_standards, required: false, type: :array, banner: 'ASFF-SECURITYHUB-STANDARDS-JSON', aliases: ['--sh', '--input-securityhub-standards']
|
162
|
+
option :output, required: true, banner: 'HDF-SCAN-RESULTS-JSON', aliases: '-o'
|
163
|
+
def asff_mapper
|
164
|
+
hdf = HeimdallTools::ASFFMapper.new(File.read(options[:json]), securityhub_standards_json_array: options[:securityhub_standards].nil? ? nil : options[:securityhub_standards].map { |filename| File.read(filename) }).to_hdf
|
165
|
+
File.write(options[:output], hdf)
|
166
|
+
puts "\rHDF Generated:\n"
|
167
|
+
puts options[:output].to_s
|
168
|
+
end
|
169
|
+
|
170
|
+
desc 'prowler_mapper', 'prowler_mapper translates Prowler-derived AWS Security Finding Format results from concatenated JSON blobs to HDF-formatted JSON so as to be viewable on Heimdall'
|
171
|
+
long_desc Help.text(:prowler_mapper)
|
172
|
+
option :json, required: true, banner: 'PROWLER-ASFF-JSON', aliases: ['-i', '--input', '-j']
|
173
|
+
option :output, required: true, banner: 'HDF-SCAN-RESULTS-JSON', aliases: '-o'
|
174
|
+
def prowler_mapper
|
175
|
+
hdf = HeimdallTools::ProwlerMapper.new(File.read(options[:json])).to_hdf
|
176
|
+
File.write(options[:output], hdf)
|
177
|
+
puts "\rHDF Generated:\n"
|
123
178
|
puts options[:output].to_s
|
124
179
|
end
|
125
180
|
|
@@ -58,9 +58,9 @@ module HeimdallTools
|
|
58
58
|
def snippet(snippetid)
|
59
59
|
snippet = @snippets.select { |x| x['id'].eql?(snippetid) }.first
|
60
60
|
"\nPath: #{snippet['File']}\n" \
|
61
|
-
|
62
|
-
|
63
|
-
|
61
|
+
"StartLine: #{snippet['StartLine']}, " \
|
62
|
+
"EndLine: #{snippet['EndLine']}\n" \
|
63
|
+
"Code:\n#{snippet['Text']['#cdata-section'].strip}" \
|
64
64
|
end
|
65
65
|
|
66
66
|
def nist_tag(rule)
|
@@ -0,0 +1,6 @@
|
|
1
|
+
asff_mapper translates AWS Security Finding Format results from JSON to HDF-formatted JSON so as to be viewable on Heimdall
|
2
|
+
|
3
|
+
Examples:
|
4
|
+
|
5
|
+
heimdall_tools asff_mapper -i <asff-finding-json> -o <hdf-scan-results-json>
|
6
|
+
heimdall_tools asff_mapper -i <asff-finding-json> --sh <standard-1-json> ... <standard-n-json> -o <hdf-scan-results-json>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
sarif_mapper translates a SARIF JSON file into HDF format JSON to be viewable in Heimdall
|
2
|
+
|
3
|
+
SARIF level to HDF impact Mapping:
|
4
|
+
SARIF level error -> HDF impact 0.7
|
5
|
+
SARIF level warning -> HDF impact 0.5
|
6
|
+
SARIF level note -> HDF impact 0.3
|
7
|
+
SARIF level none -> HDF impact 0.1
|
8
|
+
SARIF level not provided -> HDF impact 0.1 as default
|
9
|
+
|
10
|
+
Examples:
|
11
|
+
|
12
|
+
heimdall_tools sarif_mapper [OPTIONS] -j <sarif-results-json> -o <hdf-scan-results.json>
|