tenable_aws_sechub 2.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tenable_aws_sechub/__init__.py +0 -0
- tenable_aws_sechub/cli.py +42 -0
- tenable_aws_sechub/finding.py +184 -0
- tenable_aws_sechub/transform.py +194 -0
- tenable_aws_sechub/version.py +1 -0
- tenable_aws_sechub-2.2.2.dist-info/METADATA +112 -0
- tenable_aws_sechub-2.2.2.dist-info/RECORD +10 -0
- tenable_aws_sechub-2.2.2.dist-info/WHEEL +5 -0
- tenable_aws_sechub-2.2.2.dist-info/entry_points.txt +2 -0
- tenable_aws_sechub-2.2.2.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple CLI wrapper for commandline interaction with the integration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing_extensions import Annotated
|
|
8
|
+
import typer
|
|
9
|
+
import tomlkit
|
|
10
|
+
from rich.logging import RichHandler
|
|
11
|
+
from .transform import Processor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(add_completion=False)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command()
|
|
18
|
+
def sechub(
|
|
19
|
+
configfile: Path = Path('tvm2aws.toml'),
|
|
20
|
+
verbose: Annotated[int, typer.Option('--verbose', '-v', max=5, count=True)] = 2,
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Tenable to AWS Security Hub vulnerability finding importer.
|
|
24
|
+
"""
|
|
25
|
+
# Set the logging handler.
|
|
26
|
+
logging.basicConfig(
|
|
27
|
+
level=(5 - verbose) * 10,
|
|
28
|
+
datefmt='[%X]',
|
|
29
|
+
handlers=[RichHandler(rich_tracebacks=True)],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Read the configuration file
|
|
33
|
+
with configfile.open('r', encoding='utf-8') as cobj:
|
|
34
|
+
config = tomlkit.load(cobj)
|
|
35
|
+
|
|
36
|
+
# Instantiate and run the ingestion processor.
|
|
37
|
+
processor = Processor(config)
|
|
38
|
+
processor.ingest()
|
|
39
|
+
|
|
40
|
+
# Write out the updated configuration file with the updated parameters.
|
|
41
|
+
with configfile.open('w', encoding='utf-8') as cobj:
|
|
42
|
+
tomlkit.dump(processor.config, cobj)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The findings module handles transforming a TVM vulnerability finding into an
|
|
3
|
+
AWS Security Hub finding.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Dict, List
|
|
8
|
+
|
|
9
|
+
import arrow
|
|
10
|
+
from restfly.utils import dict_clean, dict_flatten, trunc
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger('sechub.finding')
|
|
13
|
+
|
|
14
|
+
SEV_MAP = {
|
|
15
|
+
0: [0, 'INFORMATIONAL'],
|
|
16
|
+
1: [10, 'LOW'],
|
|
17
|
+
2: [40, 'MEDIUM'],
|
|
18
|
+
3: [70, 'HIGH'],
|
|
19
|
+
4: [90, 'CRITICAL'],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
STATE_MAP = {
|
|
23
|
+
'OPEN': 'ACTIVE',
|
|
24
|
+
'NEW': 'ACTIVE',
|
|
25
|
+
'REOPENED': 'ACTIVE',
|
|
26
|
+
'FIXED': 'ARCHIVED',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Finding:
|
|
31
|
+
"""
|
|
32
|
+
Security Hub Finding transformer
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
region: str
|
|
36
|
+
start_date: str
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
region: str,
|
|
41
|
+
):
|
|
42
|
+
self.region = region
|
|
43
|
+
# self.account_id = account_id
|
|
44
|
+
# self.map_to_asset_account = map_to_asset_account
|
|
45
|
+
self.start_date = arrow.now().isoformat()
|
|
46
|
+
# if allowed_accounts:
|
|
47
|
+
# self.allowed_accounts = allowed_accounts
|
|
48
|
+
# elif map_to_asset_account and not allowed_accounts:
|
|
49
|
+
# self.allowed_accounts = [account_id]
|
|
50
|
+
|
|
51
|
+
def check_required_params(self, vuln: Dict):
|
|
52
|
+
"""
|
|
53
|
+
Checks to see if all of the required attributes exist for the finding
|
|
54
|
+
to be generated. If validation fails, then a KeyError will be raised
|
|
55
|
+
detailing the missing keys or keys with empty data.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
vuln (dict): The vuln object to check
|
|
59
|
+
"""
|
|
60
|
+
# We only need to specify the asset fields that are required as these
|
|
61
|
+
# fields must never have a NoneType value in them.
|
|
62
|
+
required_attributes = [
|
|
63
|
+
'asset.cloud.aws.region',
|
|
64
|
+
'asset.cloud.aws.ec2_instance_id',
|
|
65
|
+
'asset.cloud.aws.owner_id',
|
|
66
|
+
'asset.cloud.aws.ec2_instance_type',
|
|
67
|
+
'asset.cloud.aws.ec2_instance_ami_id',
|
|
68
|
+
]
|
|
69
|
+
failed = []
|
|
70
|
+
for attr in required_attributes:
|
|
71
|
+
if vuln.get(attr) is None:
|
|
72
|
+
failed.append(attr)
|
|
73
|
+
if failed:
|
|
74
|
+
raise KeyError(
|
|
75
|
+
(
|
|
76
|
+
f'The required asset attributes {",".join(failed)}'
|
|
77
|
+
f' were not set on asset {vuln["asset.uuid"]}'
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
# NOTE: Disabled the account checking as we will always be using the account
|
|
81
|
+
# owner from the asset moving forward.
|
|
82
|
+
#
|
|
83
|
+
# if (
|
|
84
|
+
# self.allowed_accounts
|
|
85
|
+
# and vuln.get('asset.aws_owner_id') not in self.allowed_accounts
|
|
86
|
+
# and self.map_to_asset_account
|
|
87
|
+
# ):
|
|
88
|
+
# raise KeyError(
|
|
89
|
+
# f'asset {vuln["asset.aws_owner_id"]}:{vuln["asset.uuid"]} is not within'
|
|
90
|
+
# ' one of the allowed accounts.'
|
|
91
|
+
# )
|
|
92
|
+
|
|
93
|
+
def generate(self, vuln: Dict) -> Dict:
|
|
94
|
+
"""
|
|
95
|
+
Generates an AWS Security Hub finding based on the TVM asset and vuln
|
|
96
|
+
finding that was inputted
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
vuln (dict): The vuln object from the vuln export
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
dict:
|
|
103
|
+
The transformed AWS Sechub finding
|
|
104
|
+
"""
|
|
105
|
+
vuln = dict_flatten(vuln)
|
|
106
|
+
self.check_required_params(vuln)
|
|
107
|
+
normalized, label = SEV_MAP[vuln.get('severity_id', 0)]
|
|
108
|
+
|
|
109
|
+
finding = {
|
|
110
|
+
'SchemaVersion': '2018-10-08',
|
|
111
|
+
'FirstObservedAt': vuln['first_found'],
|
|
112
|
+
'LastObservedAt': vuln['last_found'],
|
|
113
|
+
'ProductArn': (
|
|
114
|
+
f'arn:aws:securityhub:{self.region}::product/tenable/vulnerability-management'
|
|
115
|
+
),
|
|
116
|
+
'AwsAccountId': vuln['asset.cloud.aws.owner_id'],
|
|
117
|
+
'GeneratorId': f'tenable-plugin-{vuln["plugin.id"]}',
|
|
118
|
+
'Id': (
|
|
119
|
+
f'{vuln["asset.cloud.aws.region"]}/'
|
|
120
|
+
f'{vuln["asset.cloud.aws.ec2_instance_id"]}/'
|
|
121
|
+
f'{vuln["plugin.id"]}'
|
|
122
|
+
),
|
|
123
|
+
'CreatedAt': self.start_date,
|
|
124
|
+
'UpdatedAt': self.start_date,
|
|
125
|
+
'Types': ['Software and Configuration Checks/Vulnerabilities/CVE'],
|
|
126
|
+
'Severity': {
|
|
127
|
+
'Label': label,
|
|
128
|
+
'Normalized': normalized,
|
|
129
|
+
'Original': vuln.get('severity', 'info'),
|
|
130
|
+
},
|
|
131
|
+
# Some plugin names run quite long, we will need to truncate to
|
|
132
|
+
# the max string size that AWS supports.
|
|
133
|
+
'Title': trunc(vuln['plugin.name'], 256),
|
|
134
|
+
# The description cannot exceed 1024 characters in size.
|
|
135
|
+
'Description': trunc(vuln['plugin.description'], 1024),
|
|
136
|
+
'Resources': [
|
|
137
|
+
{
|
|
138
|
+
'Type': 'AwsEc2Instance',
|
|
139
|
+
'Id': (
|
|
140
|
+
'arn:aws:ec2:'
|
|
141
|
+
f'{vuln["asset.cloud.aws.region"]}:'
|
|
142
|
+
f'{vuln["asset.cloud.aws.owner_id"]}:'
|
|
143
|
+
'instance:'
|
|
144
|
+
f'{vuln["asset.cloud.aws.ec2_instance_id"]}'
|
|
145
|
+
),
|
|
146
|
+
'Region': vuln['asset.cloud.aws.region'],
|
|
147
|
+
'Details': {
|
|
148
|
+
'AwsEc2Instance': {
|
|
149
|
+
'Type': vuln['asset.cloud.aws.ec2_instance_type'],
|
|
150
|
+
'ImageId': vuln['asset.cloud.aws.ec2_instance_ami_id'],
|
|
151
|
+
'IpV4Addresses': vuln['asset.network.ipv4s']
|
|
152
|
+
if vuln.get('asset.network.ipv4s')
|
|
153
|
+
else None,
|
|
154
|
+
'IpV6Addresses': vuln['asset.network.ipv6s']
|
|
155
|
+
if vuln.get('asset.network.ipv6s')
|
|
156
|
+
else None,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
'ProductFields': {
|
|
162
|
+
'CVE': ', '.join(vuln.get('plugin.cve', [])),
|
|
163
|
+
'Plugin Family': vuln['plugin.family'],
|
|
164
|
+
'Type': vuln['plugin.type'],
|
|
165
|
+
},
|
|
166
|
+
# Have to research if we want to continue to construct direct
|
|
167
|
+
# links into the relevant part of the UI. This may be more
|
|
168
|
+
# complicated with the export workbenches than they were with the
|
|
169
|
+
# legacy workbenches.
|
|
170
|
+
# 'SourceUrl': 'XXXXXXX',
|
|
171
|
+
'Remediation': {
|
|
172
|
+
'Recommendation': {
|
|
173
|
+
# The solution cannot exceed 1024 characters in length.
|
|
174
|
+
'Text': trunc(vuln['plugin.solution'], 512)
|
|
175
|
+
if vuln.get('plugin.solution')
|
|
176
|
+
else None,
|
|
177
|
+
'Url': vuln.get('plugin.see_also')[0]
|
|
178
|
+
if vuln.get('plugin.see_also')
|
|
179
|
+
else None,
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
'RecordState': STATE_MAP[vuln['state']],
|
|
183
|
+
}
|
|
184
|
+
return dict_clean(finding)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transform module handles connection to both APIs and the mechanics of pushing
|
|
3
|
+
the data between them.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
import arrow
|
|
10
|
+
import boto3
|
|
11
|
+
from restfly.utils import dict_flatten, dict_merge
|
|
12
|
+
from tenable.io import TenableIO
|
|
13
|
+
|
|
14
|
+
from .finding import Finding
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Cache:
|
|
18
|
+
"""
|
|
19
|
+
AWS Cache Profile
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
session: 'boto3.session.Session'
|
|
23
|
+
client: 'boto3.session.Session'
|
|
24
|
+
cache: List[Dict]
|
|
25
|
+
region: str
|
|
26
|
+
finding: Finding
|
|
27
|
+
limit: int
|
|
28
|
+
count: int
|
|
29
|
+
|
|
30
|
+
def __init__(self, region: str, profile: str, limit: int = 100):
|
|
31
|
+
self.region = region
|
|
32
|
+
self.finding = Finding(region=region)
|
|
33
|
+
self.cache = []
|
|
34
|
+
self.count = 0
|
|
35
|
+
self.limit = limit
|
|
36
|
+
self.session = boto3.session.Session(
|
|
37
|
+
profile_name=profile,
|
|
38
|
+
region_name=region,
|
|
39
|
+
)
|
|
40
|
+
self.client = self.session.client('securityhub')
|
|
41
|
+
|
|
42
|
+
def add(self, finding: Dict):
|
|
43
|
+
"""
|
|
44
|
+
Adds the finding to the cache to be uploaded. If the cache exceeds the max
|
|
45
|
+
size limit, then we will automatically push the cache up to SecurityHub.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
finding: Merged TVM finding and asset metadata
|
|
49
|
+
"""
|
|
50
|
+
transformed = self.finding.generate(finding)
|
|
51
|
+
self.count += 1
|
|
52
|
+
self.cache.append(transformed)
|
|
53
|
+
if len(self.cache) >= self.limit:
|
|
54
|
+
self.commit()
|
|
55
|
+
|
|
56
|
+
def commit(self):
|
|
57
|
+
"""
|
|
58
|
+
Flushes the cache of findings up to Security Hub.
|
|
59
|
+
"""
|
|
60
|
+
if len(self.cache) > 0:
|
|
61
|
+
self.client.batch_import_findings(Findings=self.cache)
|
|
62
|
+
self.cache = []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Processor: # noqa PLR902
|
|
66
|
+
"""
|
|
67
|
+
Main transform processor class.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
tvm: TenableIO
|
|
71
|
+
aws_finding: Finding
|
|
72
|
+
since: int = 0
|
|
73
|
+
config: Dict
|
|
74
|
+
accounts: Dict[str, str]
|
|
75
|
+
caches: Dict[str, Cache]
|
|
76
|
+
states: List[str] = ['open', 'reopened', 'fixed']
|
|
77
|
+
sources: List[str] = ['CloudDiscoveryConnector']
|
|
78
|
+
severity: List[str] = ['high', 'critical']
|
|
79
|
+
|
|
80
|
+
def __init__(self, config: Dict):
|
|
81
|
+
# We will be maintining caches and boto3 sessions for each profile described
|
|
82
|
+
# within the configuration file. As many accounts can also exist within a
|
|
83
|
+
# singular profile, we will also have an independent dictionary to store the
|
|
84
|
+
# mapping between account and profile. This will mean that the "add" method
|
|
85
|
+
# will now automatically refuse any account_ids that arent in the mapping and
|
|
86
|
+
# instead throw a warning log message instead.
|
|
87
|
+
self.caches = {}
|
|
88
|
+
self.accounts = {}
|
|
89
|
+
for profile in config.get('aws_profile', []):
|
|
90
|
+
# For each profile defined, we will be creating a new session, client,
|
|
91
|
+
# cache, and build the account to profile linkages.
|
|
92
|
+
self.caches[profile['name']] = Cache(
|
|
93
|
+
region=profile['region'],
|
|
94
|
+
profile=profile['name'],
|
|
95
|
+
limit=config.get('upload_batch_size', 100),
|
|
96
|
+
)
|
|
97
|
+
for account in profile['accounts']:
|
|
98
|
+
self.accounts[str(account)] = profile['name']
|
|
99
|
+
|
|
100
|
+
self.config = config
|
|
101
|
+
self.since = config.get('since', self.since)
|
|
102
|
+
self.states = config.get('states', self.states)
|
|
103
|
+
self.sources = config.get('sources', self.sources)
|
|
104
|
+
self.severity = config.get('severity', self.severity)
|
|
105
|
+
self.tvm = TenableIO(
|
|
106
|
+
access_key=config.get('access_key'),
|
|
107
|
+
secret_key=config.get('secret_key'),
|
|
108
|
+
url=config.get('tvm_url', 'https://cloud.tenable.com'),
|
|
109
|
+
)
|
|
110
|
+
self._log = logging.getLogger('Tenb2SecHub')
|
|
111
|
+
self._log.debug(self.caches)
|
|
112
|
+
self._log.debug(self.accounts)
|
|
113
|
+
|
|
114
|
+
def get_trimmed_assets(self):
|
|
115
|
+
"""
|
|
116
|
+
Retreives the asset metadata and then trims out the fields that we will
|
|
117
|
+
not be needing to merge into the finding data. We do this in order to
|
|
118
|
+
ensure that we dont eat up any more memory than we need to.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
dict:
|
|
122
|
+
The dictionary of the trimmed assets.
|
|
123
|
+
"""
|
|
124
|
+
ts = arrow.utcnow().shift(days=-30).timestamp()
|
|
125
|
+
if ts > self.since:
|
|
126
|
+
ts = self.since
|
|
127
|
+
|
|
128
|
+
assets = self.tvm.exports.assets_v2(since=ts)
|
|
129
|
+
trims = [
|
|
130
|
+
'network.ipv4s',
|
|
131
|
+
'network.ipv6s',
|
|
132
|
+
'cloud.aws.region',
|
|
133
|
+
'cloud.aws.ec2_instance_id',
|
|
134
|
+
'cloud.aws.owner_id',
|
|
135
|
+
'cloud.aws.ec2_instance_type',
|
|
136
|
+
'cloud.aws.ec2_instance_ami_id',
|
|
137
|
+
]
|
|
138
|
+
trimmed = {}
|
|
139
|
+
for asset in assets:
|
|
140
|
+
a = dict_flatten(asset)
|
|
141
|
+
if a.get('cloud.aws.owner_id', None) not in self.accounts:
|
|
142
|
+
self._log.warning(f'Asset {a["id"]} not found in AWS account listing')
|
|
143
|
+
continue
|
|
144
|
+
trimmed[a['id']] = {k: v for k, v in a.items() if k in trims}
|
|
145
|
+
self._log.debug(f'Adding {a["id"]} to {a["cloud.aws.owner_id"]}')
|
|
146
|
+
return trimmed
|
|
147
|
+
|
|
148
|
+
def ingest(self):
|
|
149
|
+
"""
|
|
150
|
+
Perform the finding ingestion into AWS Security Hub.
|
|
151
|
+
"""
|
|
152
|
+
# Update the since parameter stored int he configuration to the current
|
|
153
|
+
# timestamp.
|
|
154
|
+
self.config['since'] = int(arrow.now().timestamp())
|
|
155
|
+
|
|
156
|
+
# Initiate the vulnerability export and get the trimmed asset data.
|
|
157
|
+
vulns = self.tvm.exports.vulns(
|
|
158
|
+
since=self.since, state=self.states, severity=self.severity
|
|
159
|
+
)
|
|
160
|
+
assets = self.get_trimmed_assets()
|
|
161
|
+
|
|
162
|
+
# For each vulnerability finding, we will merge in the associated
|
|
163
|
+
# asset metadata, then feed the merged information into the AWS
|
|
164
|
+
# finding generator. If we successfully got a finding generated, then
|
|
165
|
+
# add that finding to the batch queue.
|
|
166
|
+
for vuln in vulns:
|
|
167
|
+
asset_obj = assets.get(vuln['asset']['uuid'])
|
|
168
|
+
if not asset_obj:
|
|
169
|
+
self._log.warning(
|
|
170
|
+
f'No asset data for {vuln["asset"]["uuid"]} within asset cache. '
|
|
171
|
+
f'Not reporting plugin {vuln["plugin"]["id"]}.'
|
|
172
|
+
)
|
|
173
|
+
continue
|
|
174
|
+
aws_account = asset_obj['cloud.aws.owner_id']
|
|
175
|
+
profile_id = self.accounts.get(aws_account)
|
|
176
|
+
vuln['asset'] = dict_merge(vuln['asset'], asset_obj)
|
|
177
|
+
try:
|
|
178
|
+
self._log.debug(vuln)
|
|
179
|
+
self.caches[profile_id].add(vuln)
|
|
180
|
+
except Exception as err:
|
|
181
|
+
self._log.warning(
|
|
182
|
+
(
|
|
183
|
+
f'Asset {vuln["asset"]["uuid"]} finding '
|
|
184
|
+
f'{vuln["plugin"]["id"]} failed to add '
|
|
185
|
+
f'with error: {err}'
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Commit any remaining findings in the queues to AWS.
|
|
190
|
+
for _, cache in self.caches.items():
|
|
191
|
+
cache.commit()
|
|
192
|
+
|
|
193
|
+
for profile, cache in self.caches.items():
|
|
194
|
+
self._log.info(f'Sent {cache.count} events to profile {profile}')
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = '2.2.2'
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tenable_aws_sechub
|
|
3
|
+
Version: 2.2.2
|
|
4
|
+
Summary: Tenable Vulnerability Management to AWS Security Hub
|
|
5
|
+
Author-email: "Tenable, Inc." <smcgrath@tenable.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Homepage, https://github.com/tenable/Security-Hub
|
|
8
|
+
Project-URL: Repository, https://github.com/tenable/Security-Hub
|
|
9
|
+
Project-URL: Issues, https://github.com/tenable/Security-Hub/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/tenable/Security-Hub/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: tenable,tenable vulnerability management,tenable security center,tenable.io,tenable.sc,aws,security hub
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Information Technology
|
|
14
|
+
Classifier: Topic :: System :: Systems Administration
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: arrow>=1.3.0
|
|
21
|
+
Requires-Dist: restfly>=1.4.5
|
|
22
|
+
Requires-Dist: typer>=0.9.0
|
|
23
|
+
Requires-Dist: tomlkit>=0.12.4
|
|
24
|
+
Requires-Dist: rich>=13.3.1
|
|
25
|
+
Requires-Dist: boto3>=1.34.68
|
|
26
|
+
Requires-Dist: pytenable>=1.5.0
|
|
27
|
+
|
|
28
|
+
# Tenable Vulnerability Management to AWS Transformer
|
|
29
|
+
|
|
30
|
+
| :exclamation: | If upgrading from v1 please read the upgrade section |
|
|
31
|
+
| ------------- | :--------------------------------------------------- |
|
|
32
|
+
|
|
33
|
+
This tool is designed to consume Tenable.io asset and vulnerability data,
|
|
34
|
+
transform that data into the AWS Security Hub Finding format, and then upload
|
|
35
|
+
the resulting data into AWS Security Hub.
|
|
36
|
+
|
|
37
|
+
The tool can be run as either a one-shot docker container or as a command-line
|
|
38
|
+
tool. To run as a docker image, you'll need to build the image and then pass
|
|
39
|
+
the config file to the container.
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- The Tenable Vulnerability Management Integration for Security Hub must be
|
|
44
|
+
configured to accept findings from the integration.
|
|
45
|
+
- At a minimum the configuration file must have the account id that the events
|
|
46
|
+
will be sent to. Ideally the region should be configured as well.
|
|
47
|
+
- A set of API Keys within TVM should be configured to allow exportation of
|
|
48
|
+
assets and vulnerability findings from the platform. These keys should be
|
|
49
|
+
wither configured within the configuration file or as environment variables
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
pip install tenable_aws_sechub
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Upgrading from v1
|
|
58
|
+
|
|
59
|
+
If you are upgrading from the original version of the integration, please note
|
|
60
|
+
that there are some additional steps that need to be taken as the ARN that is
|
|
61
|
+
used has changed.
|
|
62
|
+
|
|
63
|
+
1. Disable the Tenable.io SecurityHub integration
|
|
64
|
+
2. Enable the Tenable Vulnerability Management integration
|
|
65
|
+
3. Configure and run the updated integration (this code)
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
Simply build a configuration file (or use the [example file][cfg] provided)
|
|
70
|
+
with the following details:
|
|
71
|
+
|
|
72
|
+
```toml
|
|
73
|
+
access_key = "1234567890abcdef1234567890"
|
|
74
|
+
secret_key = "1234567890abcdef1234567890"
|
|
75
|
+
|
|
76
|
+
[[aws_profile]]
|
|
77
|
+
name = "default"
|
|
78
|
+
accounts = [1234567890]
|
|
79
|
+
region = "us-east-1"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Once the configuration file is saved, go ahead and ensure that the AWS CLI can
|
|
83
|
+
communicate to AWS, or generate the appropriate configuration parameters that
|
|
84
|
+
AWS needs for their boto3 client. Details for how to do this is documented
|
|
85
|
+
below:
|
|
86
|
+
|
|
87
|
+
1. [Boto3 Configuration](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#configuration)
|
|
88
|
+
2. [AWS CLI Config](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)
|
|
89
|
+
|
|
90
|
+
## Running the integration
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
❯ tvm2aws --configfile /path/to/config.toml
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Commandline options
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
❯ tvm2aws --help
|
|
100
|
+
|
|
101
|
+
Usage: tvm2aws [OPTIONS]
|
|
102
|
+
|
|
103
|
+
Tenable to AWS Security Hub vulnerability finding importer.
|
|
104
|
+
|
|
105
|
+
╭─ Options ───────────────────────────────────────────────────────────────────╮
|
|
106
|
+
│ --configfile PATH [default: tvm2aws.toml] │
|
|
107
|
+
│ --verbose -v INTEGER RANGE [x<=5] [default: 2] │
|
|
108
|
+
│ --help Show this message and exit. │
|
|
109
|
+
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
[cfg]: tvm2aws.toml
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
tenable_aws_sechub/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
tenable_aws_sechub/cli.py,sha256=t8PQrWLU-3bDA5S2nTr35Jc__UvozYMHH0k0efqjmYo,1130
|
|
3
|
+
tenable_aws_sechub/finding.py,sha256=-vDA1oanhF9arhc6Q1r8dXyrCQziajM4gkPhq4xSRE4,6682
|
|
4
|
+
tenable_aws_sechub/transform.py,sha256=_e2xCn1eCHtQzemlD1CEUAjgyXFwb1anc87_ETMoAU8,6950
|
|
5
|
+
tenable_aws_sechub/version.py,sha256=WOvKCVRGotc0tr7twRMrSkDXzWTRPTrEpeC-vKBt-ls,18
|
|
6
|
+
tenable_aws_sechub-2.2.2.dist-info/METADATA,sha256=KdXJK96SC8VAsLMV78yx-BZ4DOsbF5oarnbhZ-ePijo,4455
|
|
7
|
+
tenable_aws_sechub-2.2.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
8
|
+
tenable_aws_sechub-2.2.2.dist-info/entry_points.txt,sha256=Xx4sBElx9pXAoT47aoDOF4EWrfVkAMPHOARKzkSfJ3Q,55
|
|
9
|
+
tenable_aws_sechub-2.2.2.dist-info/top_level.txt,sha256=O8dwicvnN5Vl5pBurXF5KwYsDt-BZOmv46eztwQKf4A,19
|
|
10
|
+
tenable_aws_sechub-2.2.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tenable_aws_sechub
|