tenable_aws_sechub 2.2.2__tar.gz

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.
@@ -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,85 @@
1
+ # Tenable Vulnerability Management to AWS Transformer
2
+
3
+ | :exclamation: | If upgrading from v1 please read the upgrade section |
4
+ | ------------- | :--------------------------------------------------- |
5
+
6
+ This tool is designed to consume Tenable.io asset and vulnerability data,
7
+ transform that data into the AWS Security Hub Finding format, and then upload
8
+ the resulting data into AWS Security Hub.
9
+
10
+ The tool can be run as either a one-shot docker container or as a command-line
11
+ tool. To run as a docker image, you'll need to build the image and then pass
12
+ the config file to the container.
13
+
14
+ ## Requirements
15
+
16
+ - The Tenable Vulnerability Management Integration for Security Hub must be
17
+ configured to accept findings from the integration.
18
+ - At a minimum the configuration file must have the account id that the events
19
+ will be sent to. Ideally the region should be configured as well.
20
+ - A set of API Keys within TVM should be configured to allow exportation of
21
+ assets and vulnerability findings from the platform. These keys should be
22
+ wither configured within the configuration file or as environment variables
23
+
24
+ ## Installation
25
+
26
+ ```
27
+ pip install tenable_aws_sechub
28
+ ```
29
+
30
+ ## Upgrading from v1
31
+
32
+ If you are upgrading from the original version of the integration, please note
33
+ that there are some additional steps that need to be taken as the ARN that is
34
+ used has changed.
35
+
36
+ 1. Disable the Tenable.io SecurityHub integration
37
+ 2. Enable the Tenable Vulnerability Management integration
38
+ 3. Configure and run the updated integration (this code)
39
+
40
+ ## Configuration
41
+
42
+ Simply build a configuration file (or use the [example file][cfg] provided)
43
+ with the following details:
44
+
45
+ ```toml
46
+ access_key = "1234567890abcdef1234567890"
47
+ secret_key = "1234567890abcdef1234567890"
48
+
49
+ [[aws_profile]]
50
+ name = "default"
51
+ accounts = [1234567890]
52
+ region = "us-east-1"
53
+ ```
54
+
55
+ Once the configuration file is saved, go ahead and ensure that the AWS CLI can
56
+ communicate to AWS, or generate the appropriate configuration parameters that
57
+ AWS needs for their boto3 client. Details for how to do this is documented
58
+ below:
59
+
60
+ 1. [Boto3 Configuration](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#configuration)
61
+ 2. [AWS CLI Config](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)
62
+
63
+ ## Running the integration
64
+
65
+ ```
66
+ ❯ tvm2aws --configfile /path/to/config.toml
67
+ ```
68
+
69
+ ## Commandline options
70
+
71
+ ```
72
+ ❯ tvm2aws --help
73
+
74
+ Usage: tvm2aws [OPTIONS]
75
+
76
+ Tenable to AWS Security Hub vulnerability finding importer.
77
+
78
+ ╭─ Options ───────────────────────────────────────────────────────────────────╮
79
+ │ --configfile PATH [default: tvm2aws.toml] │
80
+ │ --verbose -v INTEGER RANGE [x<=5] [default: 2] │
81
+ │ --help Show this message and exit. │
82
+ ╰─────────────────────────────────────────────────────────────────────────────╯
83
+ ```
84
+
85
+ [cfg]: tvm2aws.toml
@@ -0,0 +1,91 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ dynamic = ["version", "readme"]
7
+ name = "tenable_aws_sechub"
8
+ description = "Tenable Vulnerability Management to AWS Security Hub"
9
+ authors = [
10
+ {name = "Tenable, Inc.", email = "smcgrath@tenable.com" }
11
+ ]
12
+ keywords = [
13
+ "tenable",
14
+ "tenable vulnerability management",
15
+ "tenable security center",
16
+ "tenable.io",
17
+ "tenable.sc",
18
+ "aws",
19
+ "security hub"
20
+ ]
21
+ license = {text = "MIT License"}
22
+ classifiers = [
23
+ "Development Status :: 5 - Production/Stable",
24
+ "Intended Audience :: Information Technology",
25
+ "Topic :: System :: Systems Administration",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12"
30
+ ]
31
+ dependencies = [
32
+ "arrow>=1.3.0",
33
+ "restfly>=1.4.5",
34
+ "typer>=0.9.0",
35
+ "tomlkit>=0.12.4",
36
+ "rich>=13.3.1",
37
+ "boto3>=1.34.68",
38
+ "pytenable>=1.5.0",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/tenable/Security-Hub"
43
+ Repository = "https://github.com/tenable/Security-Hub"
44
+ Issues = "https://github.com/tenable/Security-Hub/issues"
45
+ Changelog = "https://github.com/tenable/Security-Hub/blob/main/CHANGELOG.md"
46
+
47
+ [project.scripts]
48
+ tvm2aws = "tenable_aws_sechub.cli:app"
49
+
50
+ [tool.setuptools.dynamic]
51
+ version = {attr = "tenable_aws_sechub.version.version"}
52
+ readme = {file = ["README.md"], content-type = "text/markdown"}
53
+
54
+ [tool.setuptools.packages.find]
55
+ include = ["tenable_aws_sechub*"]
56
+
57
+ [tool.ruff.lint]
58
+ select = ["E4", "E7", "E9", "F", "B"]
59
+ fixable = [ "ALL" ]
60
+ unfixable = [ "B" ]
61
+
62
+
63
+ [tool.ruff.format]
64
+ quote-style = "single"
65
+ indent-style = "space"
66
+ skip-magic-trailing-comma = false
67
+ line-ending = "lf"
68
+ docstring-code-format = false
69
+ docstring-code-line-length = "dynamic"
70
+
71
+
72
+ [tool.ruff.lint.per-file-ignores]
73
+ "__init__.py" = ["E402", "F401"]
74
+ "tenable/vm.py" = ["F401"]
75
+ "**/{tests,docs,tools}/*" = ["E402"]
76
+
77
+ [tool.pytest.ini_options]
78
+ addopts = "--cov-report term-missing --cov=tenable_aws_sechub"
79
+ pythonpath = ["."]
80
+ testpaths = ["./tests"]
81
+ filterwarnings = ["ignore:::DeprecationWarning"]
82
+
83
+ [dependency-groups]
84
+ dev = [
85
+ "moto>=5.0.28",
86
+ "ptpython>=3.0.30",
87
+ "pytest>=8.3.4",
88
+ "pytest-console-scripts>=1.4.1",
89
+ "pytest-cov>=6.0.0",
90
+ "ruff>=0.9.6",
91
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,16 @@
1
+ README.md
2
+ pyproject.toml
3
+ tenable_aws_sechub/__init__.py
4
+ tenable_aws_sechub/cli.py
5
+ tenable_aws_sechub/finding.py
6
+ tenable_aws_sechub/transform.py
7
+ tenable_aws_sechub/version.py
8
+ tenable_aws_sechub.egg-info/PKG-INFO
9
+ tenable_aws_sechub.egg-info/SOURCES.txt
10
+ tenable_aws_sechub.egg-info/dependency_links.txt
11
+ tenable_aws_sechub.egg-info/entry_points.txt
12
+ tenable_aws_sechub.egg-info/requires.txt
13
+ tenable_aws_sechub.egg-info/top_level.txt
14
+ tests/test_cli.py
15
+ tests/test_finding.py
16
+ tests/test_transform.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tvm2aws = tenable_aws_sechub.cli:app
@@ -0,0 +1,7 @@
1
+ arrow>=1.3.0
2
+ restfly>=1.4.5
3
+ typer>=0.9.0
4
+ tomlkit>=0.12.4
5
+ rich>=13.3.1
6
+ boto3>=1.34.68
7
+ pytenable>=1.5.0
@@ -0,0 +1 @@
1
+ tenable_aws_sechub
@@ -0,0 +1,9 @@
1
+ import pytest
2
+ from pytest_console_scripts import ScriptRunner
3
+ from moto import mock_aws
4
+
5
+
6
+ @pytest.mark.skip(reason='Moto doesnt mock BatchImportFindings yet')
7
+ @mock_aws
8
+ def test_cli_command(script_runner: ScriptRunner):
9
+ result = script_runner.run(['tvm2aws'])
@@ -0,0 +1,109 @@
1
+ import pytest
2
+
3
+ from tenable_aws_sechub.finding import Finding
4
+
5
+
6
+ def test_finding_required_typerror(finding_local):
7
+ """
8
+ Tests the check_required_params method and check to see if all of the
9
+ required params actually fail on this local asset.
10
+ """
11
+ f = Finding('')
12
+ with pytest.raises(
13
+ KeyError,
14
+ match=(
15
+ 'The required asset attributes '
16
+ 'asset.cloud.aws.region,'
17
+ 'asset.cloud.aws.ec2_instance_id,'
18
+ 'asset.cloud.aws.owner_id,'
19
+ 'asset.cloud.aws.ec2_instance_type,'
20
+ 'asset.cloud.aws.ec2_instance_ami_id '
21
+ 'were not set on asset '
22
+ '01234567-1234-abcd-0987-01234567890a'
23
+ ),
24
+ ):
25
+ f.check_required_params(finding_local)
26
+
27
+
28
+ def test_finding_generate_finding_keyerror(finding_local, finding_aws):
29
+ """
30
+ Tests the generate finding method and ensures that a key error is thrown
31
+ when an asset or a finding doesn't have the required parameters to generate
32
+ AWS finding.
33
+ """
34
+ f = Finding('')
35
+ with pytest.raises(KeyError):
36
+ f.generate(finding_local)
37
+
38
+ del finding_aws['plugin.type']
39
+ with pytest.raises(KeyError, match='plugin.type'):
40
+ f.generate(finding_aws)
41
+
42
+
43
+ def test_finding_generate_finding_success(finding_aws):
44
+ """
45
+ Tests the generate finding method on what should be a successful transform.
46
+ From there we will perform some minor checks to ensure that the generated
47
+ finding object looks correct.
48
+ """
49
+ f = Finding('AWS-REGION-1')
50
+ resp = f.generate(finding_aws)
51
+
52
+ # Pulled the asserts from the v1 code and made the minor modifications
53
+ # necessary for the test to pass the new finding obj.
54
+ assert resp['SchemaVersion'] == '2018-10-08'
55
+ assert resp['FirstObservedAt'] == '2018-03-22T13:29:22.070Z'
56
+ assert resp['LastObservedAt'] == '2018-12-14T12:07:38.155Z'
57
+ assert (
58
+ resp['ProductArn']
59
+ == 'arn:aws:securityhub:AWS-REGION-1::product/tenable/vulnerability-management'
60
+ )
61
+ assert resp['AwsAccountId'] == '600832220000'
62
+ assert resp['GeneratorId'] == 'tenable-plugin-106875'
63
+ assert resp['Id'] == 'us-east-1/i-00f9e618482900000/106875'
64
+ assert resp['Types'] == ['Software and Configuration Checks/Vulnerabilities/CVE']
65
+ assert resp['CreatedAt']
66
+ assert resp['UpdatedAt']
67
+ assert resp['Severity']['Product'] == 7
68
+ assert resp['Severity']['Normalized'] == 28
69
+ assert resp['Severity']['Label'] == 'HIGH'
70
+ assert resp['Title'] == 'Debian DSA-4117-1 : gcc-4.9 - security update'
71
+ assert isinstance(resp['Description'], str)
72
+ assert resp['Resources'][0]['Type'] == 'AwsEc2Instance'
73
+ assert (
74
+ resp['Resources'][0]['Id']
75
+ == 'arn:aws:ec2:us-east-1:600832220000:instance:i-00f9e618482900000'
76
+ )
77
+ assert resp['Resources'][0]['Region'] == 'us-east-1'
78
+ assert resp['Resources'][0]['Details']['AwsEc2Instance']['Type'] == 't2.medium'
79
+ assert (
80
+ resp['Resources'][0]['Details']['AwsEc2Instance']['ImageId'] == 'ami-daf89000'
81
+ )
82
+ assert resp['Resources'][0]['Details']['AwsEc2Instance']['IpV4Addresses'] == [
83
+ '192.168.101.249'
84
+ ]
85
+ assert 'IpV6Addresses' not in resp['Resources'][0]['Details']['AwsEc2Instance']
86
+ assert resp['ProductFields']['CVE'] == ''
87
+ assert resp['ProductFields']['Plugin Family'] == 'Debian Local Security Checks'
88
+ assert resp['ProductFields']['Type'] == 'local'
89
+ assert isinstance(resp['Remediation']['Recommendation']['Text'], str)
90
+ assert (
91
+ resp['Remediation']['Recommendation']['Url']
92
+ == 'https://security-tracker.debian.org/tracker/source-package/gcc-4.9'
93
+ )
94
+ assert resp['RecordState'] == 'ACTIVE'
95
+
96
+ # Also check to make sure that a fixed finding returns an "ARCHIVED"
97
+ # record state.
98
+ finding_aws['state'] = 'FIXED'
99
+ resp = f.generate(finding_aws)
100
+ assert resp['RecordState'] == 'ARCHIVED'
101
+
102
+
103
+ def test_generate_finding_none_url(finding_aws):
104
+ f = Finding('')
105
+
106
+ del finding_aws['plugin.see_also']
107
+ resp = f.generate(finding_aws)
108
+
109
+ assert 'Url' not in resp['Remediation']['Recommendation']
@@ -0,0 +1,102 @@
1
+ import logging
2
+
3
+ import pytest
4
+ import responses
5
+ from moto import mock_aws
6
+
7
+ from tenable_aws_sechub.transform import Processor
8
+
9
+
10
+ @pytest.fixture
11
+ def config():
12
+ return {
13
+ 'since': 0,
14
+ 'access_key': '1234567890abcdef1234567890',
15
+ 'secret_key': '1234567890abcdef1234567890',
16
+ 'aws_profile': [
17
+ {
18
+ 'name': 'default',
19
+ 'region': 'us-east-1',
20
+ 'accounts': ['600832220000'],
21
+ }
22
+ ],
23
+ }
24
+
25
+
26
+ @responses.activate
27
+ @mock_aws
28
+ def test_processor_asset_trim(asset_aws, config):
29
+ id = '12345678-1234-1234-1234-1234567890ab'
30
+ responses.post(
31
+ 'https://cloud.tenable.com/assets/v2/export', json={'export_uuid': id}
32
+ )
33
+ responses.get(
34
+ f'https://cloud.tenable.com/assets/export/{id}/status',
35
+ json={'status': 'FINISHED', 'chunks_available': [1]},
36
+ )
37
+ responses.get(
38
+ f'https://cloud.tenable.com/assets/export/{id}/chunks/1', json=[asset_aws]
39
+ )
40
+ p = Processor(config)
41
+
42
+ assets = p.get_trimmed_assets()
43
+ asset_id = '01234567-1234-abcd-0987-01234567890b'
44
+ assert isinstance(assets, dict)
45
+ assert asset_id in assets
46
+ assert 'cloud.aws.subnet_id' not in assets[asset_id].keys()
47
+ assert 'cloud.aws.owner_id' in assets[asset_id].keys()
48
+
49
+
50
+ @responses.activate
51
+ @mock_aws
52
+ def test_processor_ingest_warn(asset_local, finding, config, caplog):
53
+ id = '12345678-1234-1234-1234-1234567890ab'
54
+ finding['asset']['uuid'] = asset_local['id']
55
+ responses.post(
56
+ 'https://cloud.tenable.com/assets/v2/export', json={'export_uuid': id}
57
+ )
58
+ responses.get(
59
+ f'https://cloud.tenable.com/assets/export/{id}/status',
60
+ json={'status': 'FINISHED', 'chunks_available': [1]},
61
+ )
62
+ responses.get(
63
+ f'https://cloud.tenable.com/assets/export/{id}/chunks/1', json=[asset_local]
64
+ )
65
+ responses.post('https://cloud.tenable.com/vulns/export', json={'export_uuid': id})
66
+ responses.get(
67
+ f'https://cloud.tenable.com/vulns/export/{id}/status',
68
+ json={'status': 'FINISHED', 'chunks_available': [1]},
69
+ )
70
+ responses.get(
71
+ f'https://cloud.tenable.com/vulns/export/{id}/chunks/1', json=[finding]
72
+ )
73
+ p = Processor(config)
74
+ with caplog.at_level(logging.WARNING, logger='Tenb2SecHub'):
75
+ p.ingest()
76
+ msg = f'Asset {asset_local["id"]} not found in AWS account listing'
77
+ assert msg in [r.message for r in caplog.records]
78
+
79
+
80
+ @pytest.mark.skip(reason='BatchImportFindings is not yet implemented in moto')
81
+ @responses.activate
82
+ @mock_aws
83
+ def test_processor_ingest(asset_aws, finding, config):
84
+ id = '12345678-1234-1234-1234-1234567890ab'
85
+ responses.post('https://cloud.tenable.com/assets/export', json={'export_uuid': id})
86
+ responses.get(
87
+ f'https://cloud.tenable.com/assets/export/{id}/status',
88
+ json={'status': 'FINISHED', 'chunks_available': [1]},
89
+ )
90
+ responses.get(
91
+ f'https://cloud.tenable.com/assets/export/{id}/chunks/1', json=[asset_aws]
92
+ )
93
+ responses.post('https://cloud.tenable.com/vulns/export', json={'export_uuid': id})
94
+ responses.get(
95
+ f'https://cloud.tenable.com/vulns/export/{id}/status',
96
+ json={'status': 'FINISHED', 'chunks_available': [1]},
97
+ )
98
+ responses.get(
99
+ f'https://cloud.tenable.com/vulns/export/{id}/chunks/1', json=[finding]
100
+ )
101
+ p = Processor(config)
102
+ p.ingest()