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.
- tenable_aws_sechub-2.2.2/PKG-INFO +112 -0
- tenable_aws_sechub-2.2.2/README.md +85 -0
- tenable_aws_sechub-2.2.2/pyproject.toml +91 -0
- tenable_aws_sechub-2.2.2/setup.cfg +4 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub/__init__.py +0 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub/cli.py +42 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub/finding.py +184 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub/transform.py +194 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub/version.py +1 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub.egg-info/PKG-INFO +112 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub.egg-info/SOURCES.txt +16 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub.egg-info/dependency_links.txt +1 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub.egg-info/entry_points.txt +2 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub.egg-info/requires.txt +7 -0
- tenable_aws_sechub-2.2.2/tenable_aws_sechub.egg-info/top_level.txt +1 -0
- tenable_aws_sechub-2.2.2/tests/test_cli.py +9 -0
- tenable_aws_sechub-2.2.2/tests/test_finding.py +109 -0
- tenable_aws_sechub-2.2.2/tests/test_transform.py +102 -0
|
@@ -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
|
+
]
|
|
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,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 @@
|
|
|
1
|
+
|
|
@@ -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()
|