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.
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tvm2aws = tenable_aws_sechub.cli:app
@@ -0,0 +1 @@
1
+ tenable_aws_sechub