auditops 0.1.0__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.
- auditops-0.1.0/LICENSE +21 -0
- auditops-0.1.0/PKG-INFO +41 -0
- auditops-0.1.0/README.md +17 -0
- auditops-0.1.0/pyproject.toml +46 -0
- auditops-0.1.0/setup.cfg +4 -0
- auditops-0.1.0/src/auditops/core/__init__.py +17 -0
- auditops-0.1.0/src/auditops/core/evidence/reader.py +30 -0
- auditops-0.1.0/src/auditops/core/evidence/writer.py +23 -0
- auditops-0.1.0/src/auditops/core/exclusions.py +109 -0
- auditops-0.1.0/src/auditops/core/models.py +238 -0
- auditops-0.1.0/src/auditops/core/reporting/pdf_report_builder.py +263 -0
- auditops-0.1.0/src/auditops/core/reporting/styles.py +60 -0
- auditops-0.1.0/src/auditops/core/uploader.py +28 -0
- auditops-0.1.0/src/auditops/core/utils.py +122 -0
- auditops-0.1.0/src/auditops/providers/aws/__init__.py +5 -0
- auditops-0.1.0/src/auditops/providers/aws/aws_collector.py +114 -0
- auditops-0.1.0/src/auditops/providers/aws/aws_config.py +76 -0
- auditops-0.1.0/src/auditops/providers/aws/aws_tester.py +89 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/__init__.py +15 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/account.py +8 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/apigateway.py +16 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/cloudtrail.py +21 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/ec2.py +51 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/elbv2.py +16 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/guardduty.py +26 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/iam.py +83 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/lmbda.py +38 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/rds.py +22 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/s3.py +51 -0
- auditops-0.1.0/src/auditops/providers/aws/collectors/wafv2.py +26 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/cloudtrail/__init__.py +3 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/cloudtrail/multi_region.py +43 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/ebs/__init__.py +5 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/ebs/default_encryption.py +40 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/ebs/volume_encryption.py +45 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/ebs/volume_tags.py +52 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/ec2/__init__.py +5 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/ec2/instance_tags.py +52 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/ec2/security_group_tags.py +51 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/guardduty/__init__.py +3 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/guardduty/enabled.py +61 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/iam/__init__.py +7 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/iam/password_policy.py +76 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/iam/root_access_key.py +31 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/iam/root_mfa.py +29 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/iam/user_access_key_age.py +80 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/iam/user_mfa.py +60 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/lmbda/__init__.py +3 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/lmbda/tags.py +56 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/rds/__init__.py +9 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/rds/auto_minor_version_upgrade.py +45 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/rds/backup_retention.py +48 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/rds/deletion_protection.py +68 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/rds/encryption.py +45 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/rds/public_access.py +46 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/rds/tags.py +51 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/s3/__init__.py +7 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/s3/encryption.py +51 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/s3/public_access.py +54 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/s3/secure_transport.py +71 -0
- auditops-0.1.0/src/auditops/providers/aws/tests/s3/tags.py +59 -0
- auditops-0.1.0/src/auditops/providers/github/__init__.py +4 -0
- auditops-0.1.0/src/auditops/providers/github/github_collector.py +62 -0
- auditops-0.1.0/src/auditops/providers/github/github_tester.py +33 -0
- auditops-0.1.0/src/auditops/providers/github/tests/org.py +57 -0
- auditops-0.1.0/src/auditops/providers/github/tests/repos.py +37 -0
- auditops-0.1.0/src/auditops/providers/google_workspace/__init__.py +3 -0
- auditops-0.1.0/src/auditops/providers/google_workspace/google_workspace_collector.py +28 -0
- auditops-0.1.0/src/auditops.egg-info/PKG-INFO +41 -0
- auditops-0.1.0/src/auditops.egg-info/SOURCES.txt +71 -0
- auditops-0.1.0/src/auditops.egg-info/dependency_links.txt +1 -0
- auditops-0.1.0/src/auditops.egg-info/requires.txt +3 -0
- auditops-0.1.0/src/auditops.egg-info/top_level.txt +1 -0
auditops-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AuditOps LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
auditops-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: auditops
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open-source evidence collection framework.
|
|
5
|
+
Author: AJ Dehn
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/AuditOps/auditops-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/AuditOps/auditops-sdk/issues
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Information Technology
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: boto3>=1.40
|
|
21
|
+
Requires-Dist: reportlab>=4.0
|
|
22
|
+
Requires-Dist: requests>=2.34
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Purpose
|
|
26
|
+
Gather evidence, perform testing, and create audit reports in ~25 lines of code per tool.
|
|
27
|
+
|
|
28
|
+
## Use cases:
|
|
29
|
+
- Vendor Due Diligence: Evaluate your vendors actual security posture (transparent and timely reports allows you to truly evaluate risk).
|
|
30
|
+
- Audits: Automatically collect and upload high quality evidence directly to your auditor (no screenshots required).
|
|
31
|
+
|
|
32
|
+
## What this library does:
|
|
33
|
+
1. Evidence Collection: Gathers evidence (JSON files) directly from your vendor's API or SDK.
|
|
34
|
+
2. Testing: Logic based configuration tests (encryption, MFA, key rotation, etc) against your cloud environment.
|
|
35
|
+
3. Report Building: Builds JSON & PDF reports that summarize findings from each test & tool.
|
|
36
|
+
4. Upload: Share full audit packages (evidence & audit reports) with auditors, customers, and regulators.
|
|
37
|
+
|
|
38
|
+
## Philosophy:
|
|
39
|
+
1. Lightweight: You can setup this library in minutes, and it doesn't require intensive integrations with your cloud providers. This eliminates vendor lock-in and gives you control over how your data is processed.
|
|
40
|
+
2. Repeatability: Clear instructions explain how evidence was gathered and describes the test procedures that were performed. Share your AuditOps report + supporting evidence, and kindly ask your auditor to re-perform the work that was already done.
|
|
41
|
+
3. Anti-Checkbox: We all know that compliance has become a check-box exercise. We hope you'll use this library to fight against it and start holding everyone to higher standard.
|
auditops-0.1.0/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Purpose
|
|
2
|
+
Gather evidence, perform testing, and create audit reports in ~25 lines of code per tool.
|
|
3
|
+
|
|
4
|
+
## Use cases:
|
|
5
|
+
- Vendor Due Diligence: Evaluate your vendors actual security posture (transparent and timely reports allows you to truly evaluate risk).
|
|
6
|
+
- Audits: Automatically collect and upload high quality evidence directly to your auditor (no screenshots required).
|
|
7
|
+
|
|
8
|
+
## What this library does:
|
|
9
|
+
1. Evidence Collection: Gathers evidence (JSON files) directly from your vendor's API or SDK.
|
|
10
|
+
2. Testing: Logic based configuration tests (encryption, MFA, key rotation, etc) against your cloud environment.
|
|
11
|
+
3. Report Building: Builds JSON & PDF reports that summarize findings from each test & tool.
|
|
12
|
+
4. Upload: Share full audit packages (evidence & audit reports) with auditors, customers, and regulators.
|
|
13
|
+
|
|
14
|
+
## Philosophy:
|
|
15
|
+
1. Lightweight: You can setup this library in minutes, and it doesn't require intensive integrations with your cloud providers. This eliminates vendor lock-in and gives you control over how your data is processed.
|
|
16
|
+
2. Repeatability: Clear instructions explain how evidence was gathered and describes the test procedures that were performed. Share your AuditOps report + supporting evidence, and kindly ask your auditor to re-perform the work that was already done.
|
|
17
|
+
3. Anti-Checkbox: We all know that compliance has become a check-box exercise. We hope you'll use this library to fight against it and start holding everyone to higher standard.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "auditops"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Open-source evidence collection framework."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Information Technology",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
dependencies = [
|
|
25
|
+
"boto3>=1.40",
|
|
26
|
+
"reportlab>=4.0",
|
|
27
|
+
"requests>=2.34"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
authors = [
|
|
31
|
+
{name = "AJ Dehn"}
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Repository = "https://github.com/AuditOps/auditops-sdk"
|
|
36
|
+
Issues = "https://github.com/AuditOps/auditops-sdk/issues"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
package-dir = {"" = "src"}
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
exclude = ["tests*", "docs*", "env*", "venv*"]
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.exclude-package-data]
|
|
46
|
+
"*" = [".env", "*.pyc", "__pycache__", "config.json"]
|
auditops-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
2
|
+
|
|
3
|
+
from .evidence.writer import EvidenceWriter
|
|
4
|
+
from .evidence.reader import EvidenceReader
|
|
5
|
+
from .models import Test, Audit
|
|
6
|
+
from .uploader import Uploader
|
|
7
|
+
from .reporting.pdf_report_builder import PDFReportBuilder
|
|
8
|
+
from .exclusions import ExclusionManager
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"EvidenceWriter",
|
|
12
|
+
"EvidenceReader",
|
|
13
|
+
"Uploader",
|
|
14
|
+
"PDFReportBuilder",
|
|
15
|
+
"Test",
|
|
16
|
+
"ExclusionManager"
|
|
17
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EvidenceReader:
|
|
6
|
+
def __init__(self, root_dir="tmp"):
|
|
7
|
+
self.root_dir = Path(root_dir)
|
|
8
|
+
self.evidence_dir = self.root_dir / "audit_evidence"
|
|
9
|
+
|
|
10
|
+
def _path(self, relative_path):
|
|
11
|
+
return self.evidence_dir / relative_path
|
|
12
|
+
|
|
13
|
+
def read_json(self, relative_path, optional=False):
|
|
14
|
+
"""
|
|
15
|
+
Read a JSON evidence file.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
relative_path: Path relative from the audit's evidence folder.
|
|
19
|
+
optional: Return None instead of raising if the file is missing.
|
|
20
|
+
"""
|
|
21
|
+
path = self._path(relative_path)
|
|
22
|
+
|
|
23
|
+
if not path.exists():
|
|
24
|
+
if optional:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
raise FileNotFoundError(f"Missing required evidence: {relative_path}")
|
|
28
|
+
|
|
29
|
+
with path.open() as f:
|
|
30
|
+
return json.load(f)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logging.basicConfig(
|
|
6
|
+
level=logging.INFO,
|
|
7
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
class EvidenceWriter:
|
|
11
|
+
def __init__(self, root_dir="tmp"):
|
|
12
|
+
self.root_dir = Path(root_dir)
|
|
13
|
+
self.evidence_dir = self.root_dir / "audit_evidence"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def save_json(self, relative_path, data):
|
|
17
|
+
if data:
|
|
18
|
+
file_path = self.evidence_dir / relative_path
|
|
19
|
+
|
|
20
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
with file_path.open("w") as f:
|
|
23
|
+
json.dump(data, f, indent=4, default=str)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import date
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import fnmatch
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Exclusion:
|
|
10
|
+
provider: str
|
|
11
|
+
test_id: str
|
|
12
|
+
rationale: str
|
|
13
|
+
sample_id: dict | None = None
|
|
14
|
+
expires: date | None = None
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def is_expired(self) -> bool:
|
|
18
|
+
return self.expires is not None and self.expires < date.today()
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def is_pattern(self) -> bool:
|
|
22
|
+
return self.sample_id is not None and any(
|
|
23
|
+
"*" in str(v) for v in self.sample_id.values()
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def matches_sample(self, sample_id: dict) -> bool:
|
|
27
|
+
"""Only valid for pattern exclusions."""
|
|
28
|
+
for field, pattern in self.sample_id.items():
|
|
29
|
+
value = sample_id.get(field)
|
|
30
|
+
if value is None or not fnmatch.fnmatch(str(value), str(pattern)):
|
|
31
|
+
return False
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ExclusionManager:
|
|
36
|
+
def __init__(self, exclusions: list[Exclusion] | None = None):
|
|
37
|
+
self._test_exclusions: dict[tuple, Exclusion] = {}
|
|
38
|
+
self._sample_exclusions: dict[tuple, Exclusion] = {}
|
|
39
|
+
self._sample_patterns: dict[tuple, list[Exclusion]] = defaultdict(list)
|
|
40
|
+
|
|
41
|
+
for exclusion in exclusions or []:
|
|
42
|
+
if exclusion.is_expired:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
key_prefix = (exclusion.provider, exclusion.test_id)
|
|
46
|
+
|
|
47
|
+
if exclusion.sample_id is None:
|
|
48
|
+
self._test_exclusions[key_prefix] = exclusion
|
|
49
|
+
elif exclusion.is_pattern:
|
|
50
|
+
self._sample_patterns[key_prefix].append(exclusion)
|
|
51
|
+
else:
|
|
52
|
+
key = key_prefix + (frozenset(exclusion.sample_id.items()),)
|
|
53
|
+
self._sample_exclusions[key] = exclusion
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def load_exclusions(cls, filename: str | Path):
|
|
57
|
+
filename = Path(filename)
|
|
58
|
+
|
|
59
|
+
if not filename.exists():
|
|
60
|
+
return cls()
|
|
61
|
+
|
|
62
|
+
with open(filename, "r") as f:
|
|
63
|
+
data = json.load(f)
|
|
64
|
+
|
|
65
|
+
exclusions = []
|
|
66
|
+
for item in data.get("exclusions", []):
|
|
67
|
+
expires = item.get("expires")
|
|
68
|
+
if expires:
|
|
69
|
+
expires = date.fromisoformat(expires)
|
|
70
|
+
|
|
71
|
+
exclusions.append(
|
|
72
|
+
Exclusion(
|
|
73
|
+
provider=item["provider"].lower(),
|
|
74
|
+
test_id=item["test_id"],
|
|
75
|
+
sample_id=item.get("sample_id"),
|
|
76
|
+
rationale=item["rationale"],
|
|
77
|
+
expires=expires,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return cls(exclusions)
|
|
82
|
+
|
|
83
|
+
def get_test_exclusion(self, provider: str, test_id: str) -> Exclusion | None:
|
|
84
|
+
return self._test_exclusions.get(
|
|
85
|
+
(provider.lower(), test_id)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get_sample_exclusion(self, provider: str, test_id: str, sample_id: dict) -> Exclusion | None:
|
|
89
|
+
provider = provider.lower()
|
|
90
|
+
|
|
91
|
+
# Exact match first (fast)
|
|
92
|
+
key = (provider.lower(), test_id, frozenset(sample_id.items()))
|
|
93
|
+
|
|
94
|
+
exclusion = self._sample_exclusions.get(key)
|
|
95
|
+
if exclusion:
|
|
96
|
+
return exclusion
|
|
97
|
+
|
|
98
|
+
# Pattern match — only checks patterns registered under this (provider, test_id)
|
|
99
|
+
for exclusion in self._sample_patterns.get((provider, test_id), []):
|
|
100
|
+
if exclusion.matches_sample(sample_id):
|
|
101
|
+
return exclusion
|
|
102
|
+
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def is_test_excluded(self, provider: str, test_id: str) -> bool:
|
|
106
|
+
return self.get_test_exclusion(provider, test_id) is not None
|
|
107
|
+
|
|
108
|
+
def is_sample_excluded(self, provider: str, test_id: str, sample_id: dict) -> bool:
|
|
109
|
+
return self.get_sample_exclusion(provider, test_id, sample_id) is not None
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List, Optional, Dict, Any
|
|
3
|
+
from datetime import date
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from .reporting.pdf_report_builder import PDFReportBuilder
|
|
7
|
+
from .evidence.reader import EvidenceReader
|
|
8
|
+
from .evidence.writer import EvidenceWriter
|
|
9
|
+
from .exclusions import ExclusionManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AuditHelpers:
|
|
14
|
+
reader: EvidenceReader
|
|
15
|
+
writer: EvidenceWriter
|
|
16
|
+
exclusions: ExclusionManager
|
|
17
|
+
report_builder: PDFReportBuilder
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def create(cls, exclusions_file: str | None = None):
|
|
21
|
+
return cls(
|
|
22
|
+
reader=EvidenceReader(),
|
|
23
|
+
writer=EvidenceWriter(),
|
|
24
|
+
exclusions=(
|
|
25
|
+
ExclusionManager.load_exclusions(exclusions_file)
|
|
26
|
+
if exclusions_file
|
|
27
|
+
else ExclusionManager()
|
|
28
|
+
),
|
|
29
|
+
report_builder=PDFReportBuilder(),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class AuditContext:
|
|
35
|
+
provider: str
|
|
36
|
+
helpers: AuditHelpers
|
|
37
|
+
|
|
38
|
+
config: object | None = None
|
|
39
|
+
evidence_folder: str = ""
|
|
40
|
+
report_name: str = ""
|
|
41
|
+
auditor_name: str = "AJ Dehn"
|
|
42
|
+
delete_cached_evidence: bool = True
|
|
43
|
+
summary_mode: bool = False
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def reader(self):
|
|
47
|
+
return self.helpers.reader
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def writer(self):
|
|
51
|
+
return self.helpers.writer
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def exclusions(self):
|
|
55
|
+
return self.helpers.exclusions
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def report_builder(self):
|
|
59
|
+
return self.helpers.report_builder
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def report_dir(self) -> Path:
|
|
63
|
+
path = Path(self.reader.root_dir) / "reports"
|
|
64
|
+
if self.evidence_folder:
|
|
65
|
+
path /= self.evidence_folder
|
|
66
|
+
return path
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def json_report_path(self) -> Path:
|
|
70
|
+
return self.report_dir / f"{self.report_name}.json"
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def pdf_report_path(self) -> Path:
|
|
74
|
+
return self.report_dir / f"{self.report_name}.pdf"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Audit:
|
|
78
|
+
def __init__(self, title=None, auditor_name="AuditOps"):
|
|
79
|
+
self.test_results = None
|
|
80
|
+
self.title = title
|
|
81
|
+
self.auditor_name = auditor_name
|
|
82
|
+
self.scope = None
|
|
83
|
+
|
|
84
|
+
def update_scope(self, updated_scope):
|
|
85
|
+
self.scope = updated_scope
|
|
86
|
+
|
|
87
|
+
def get_scope(self):
|
|
88
|
+
return self.scope
|
|
89
|
+
|
|
90
|
+
def get_scope_formatted(self):
|
|
91
|
+
html = []
|
|
92
|
+
|
|
93
|
+
for item in self.scope:
|
|
94
|
+
if ":" in item:
|
|
95
|
+
label, value = item.split(":", 1)
|
|
96
|
+
html.append(f"<b>{label}:</b> {value}")
|
|
97
|
+
else:
|
|
98
|
+
html.append(item)
|
|
99
|
+
|
|
100
|
+
html_output = "<br/>".join(html)
|
|
101
|
+
|
|
102
|
+
return html_output
|
|
103
|
+
|
|
104
|
+
def to_dict(self):
|
|
105
|
+
return {
|
|
106
|
+
"metadata": {
|
|
107
|
+
"scope": self.scope,
|
|
108
|
+
"report_date": datetime.now(timezone.utc).strftime('%Y-%m-%d')
|
|
109
|
+
},
|
|
110
|
+
"test_results": [t.to_dict() for t in self.test_results]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# NOTE: Samples default to "is_passing: False" until logic determines sample passes the testing criteria.
|
|
115
|
+
@dataclass
|
|
116
|
+
class Sample:
|
|
117
|
+
sample_id: Dict[str, Any]
|
|
118
|
+
is_excluded: bool = False
|
|
119
|
+
is_passing: bool = False
|
|
120
|
+
comments: str = ""
|
|
121
|
+
|
|
122
|
+
def __str__(self):
|
|
123
|
+
return (
|
|
124
|
+
f"sample_id: {self.sample_id}\n"
|
|
125
|
+
f"is_excluded: {self.is_excluded}\n"
|
|
126
|
+
f"is_passing: {self.is_passing}\n"
|
|
127
|
+
f"comments: {self.comments}\n"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def to_dict(self):
|
|
131
|
+
return {
|
|
132
|
+
"sample_id": self.sample_id,
|
|
133
|
+
"is_excluded": self.is_excluded,
|
|
134
|
+
"is_passing": self.is_passing,
|
|
135
|
+
"comments": self.comments,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# NOTE: Tests default to "is_passing: True" until there is a failing sample or other logic determines the test has failed.
|
|
140
|
+
@dataclass
|
|
141
|
+
class Test:
|
|
142
|
+
test_id: str
|
|
143
|
+
test_description: str
|
|
144
|
+
test_procedures: List[str]
|
|
145
|
+
test_attributes: List[str]
|
|
146
|
+
# Rating Matrix: 0 - Informational, 1 - Low, 2 - Medium, 3 - High.
|
|
147
|
+
risk_rating: int
|
|
148
|
+
table_headers: Optional[List[str]] = None
|
|
149
|
+
samples: List["Sample"] = field(default_factory=list)
|
|
150
|
+
is_passing: bool = True
|
|
151
|
+
is_excluded: bool = False
|
|
152
|
+
comments: str = ""
|
|
153
|
+
num_findings: int = 0
|
|
154
|
+
num_exclusions: int = 0
|
|
155
|
+
num_passing: int = 0
|
|
156
|
+
total_population: int = 0
|
|
157
|
+
|
|
158
|
+
def __str__(self):
|
|
159
|
+
return (
|
|
160
|
+
f"test_id: {self.test_id}\n"
|
|
161
|
+
f"test_description: {self.test_description}\n"
|
|
162
|
+
f"risk_rating: {self.risk_rating}\n"
|
|
163
|
+
f"is_passing: {self.is_passing}\n"
|
|
164
|
+
f"comments: {self.comments}\n"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def to_dict(self):
|
|
169
|
+
result = {
|
|
170
|
+
"test_id": self.test_id,
|
|
171
|
+
"is_excluded": self.is_excluded,
|
|
172
|
+
"test_description": self.test_description,
|
|
173
|
+
"risk_rating": self.risk_rating,
|
|
174
|
+
"is_passing": self.is_passing,
|
|
175
|
+
"comments": self.comments,
|
|
176
|
+
"test_procedures": self.test_procedures,
|
|
177
|
+
"test_attributes": self.test_attributes,
|
|
178
|
+
}
|
|
179
|
+
# Include samples, if present.
|
|
180
|
+
if self.samples:
|
|
181
|
+
result["samples"] = [s.to_dict() for s in self.samples]
|
|
182
|
+
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_risk_rating_str(self):
|
|
187
|
+
if self.risk_rating == 0: return "Informational"
|
|
188
|
+
elif self.risk_rating == 1: return "Low"
|
|
189
|
+
elif self.risk_rating == 2: return "Medium"
|
|
190
|
+
elif self.risk_rating == 3: return "High"
|
|
191
|
+
else:
|
|
192
|
+
raise ValueError(f"Invalid risk rating: {self.risk_rating}. Accepted values are 0 - 3.")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def add_sample(self, sample):
|
|
196
|
+
self.samples.append(sample)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def evaluate_samples(self, exclusions=None, provider=None, test_id = None, failure_message: str = None):
|
|
200
|
+
self.total_population = len(self.samples)
|
|
201
|
+
self.num_exclusions = 0
|
|
202
|
+
self.num_findings = 0
|
|
203
|
+
self.num_passing = 0
|
|
204
|
+
|
|
205
|
+
for sample in self.samples:
|
|
206
|
+
if exclusions:
|
|
207
|
+
exclusion = exclusions.get_sample_exclusion(provider, self.test_id, sample.sample_id)
|
|
208
|
+
if exclusion:
|
|
209
|
+
sample.is_excluded = True
|
|
210
|
+
sample.comments = exclusion.rationale
|
|
211
|
+
|
|
212
|
+
if sample.is_excluded:
|
|
213
|
+
self.num_exclusions += 1
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
if not sample.is_passing:
|
|
217
|
+
self.num_findings += 1
|
|
218
|
+
|
|
219
|
+
self.is_passing = self.num_findings == 0
|
|
220
|
+
|
|
221
|
+
self.num_passing = self.total_population - self.num_findings - self.num_exclusions
|
|
222
|
+
|
|
223
|
+
if failure_message:
|
|
224
|
+
self.set_failure_summary(failure_message)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def set_failure_summary(self, message: str):
|
|
228
|
+
if self.is_passing:
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
# Make sure message ends with a period.
|
|
232
|
+
message = message.rstrip()
|
|
233
|
+
if not message.endswith("."):
|
|
234
|
+
message += "."
|
|
235
|
+
|
|
236
|
+
self.comments = (
|
|
237
|
+
f"Exceptions Noted. {self.num_findings} of {self.total_population} {message}"
|
|
238
|
+
)
|