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.
Files changed (73) hide show
  1. auditops-0.1.0/LICENSE +21 -0
  2. auditops-0.1.0/PKG-INFO +41 -0
  3. auditops-0.1.0/README.md +17 -0
  4. auditops-0.1.0/pyproject.toml +46 -0
  5. auditops-0.1.0/setup.cfg +4 -0
  6. auditops-0.1.0/src/auditops/core/__init__.py +17 -0
  7. auditops-0.1.0/src/auditops/core/evidence/reader.py +30 -0
  8. auditops-0.1.0/src/auditops/core/evidence/writer.py +23 -0
  9. auditops-0.1.0/src/auditops/core/exclusions.py +109 -0
  10. auditops-0.1.0/src/auditops/core/models.py +238 -0
  11. auditops-0.1.0/src/auditops/core/reporting/pdf_report_builder.py +263 -0
  12. auditops-0.1.0/src/auditops/core/reporting/styles.py +60 -0
  13. auditops-0.1.0/src/auditops/core/uploader.py +28 -0
  14. auditops-0.1.0/src/auditops/core/utils.py +122 -0
  15. auditops-0.1.0/src/auditops/providers/aws/__init__.py +5 -0
  16. auditops-0.1.0/src/auditops/providers/aws/aws_collector.py +114 -0
  17. auditops-0.1.0/src/auditops/providers/aws/aws_config.py +76 -0
  18. auditops-0.1.0/src/auditops/providers/aws/aws_tester.py +89 -0
  19. auditops-0.1.0/src/auditops/providers/aws/collectors/__init__.py +15 -0
  20. auditops-0.1.0/src/auditops/providers/aws/collectors/account.py +8 -0
  21. auditops-0.1.0/src/auditops/providers/aws/collectors/apigateway.py +16 -0
  22. auditops-0.1.0/src/auditops/providers/aws/collectors/cloudtrail.py +21 -0
  23. auditops-0.1.0/src/auditops/providers/aws/collectors/ec2.py +51 -0
  24. auditops-0.1.0/src/auditops/providers/aws/collectors/elbv2.py +16 -0
  25. auditops-0.1.0/src/auditops/providers/aws/collectors/guardduty.py +26 -0
  26. auditops-0.1.0/src/auditops/providers/aws/collectors/iam.py +83 -0
  27. auditops-0.1.0/src/auditops/providers/aws/collectors/lmbda.py +38 -0
  28. auditops-0.1.0/src/auditops/providers/aws/collectors/rds.py +22 -0
  29. auditops-0.1.0/src/auditops/providers/aws/collectors/s3.py +51 -0
  30. auditops-0.1.0/src/auditops/providers/aws/collectors/wafv2.py +26 -0
  31. auditops-0.1.0/src/auditops/providers/aws/tests/cloudtrail/__init__.py +3 -0
  32. auditops-0.1.0/src/auditops/providers/aws/tests/cloudtrail/multi_region.py +43 -0
  33. auditops-0.1.0/src/auditops/providers/aws/tests/ebs/__init__.py +5 -0
  34. auditops-0.1.0/src/auditops/providers/aws/tests/ebs/default_encryption.py +40 -0
  35. auditops-0.1.0/src/auditops/providers/aws/tests/ebs/volume_encryption.py +45 -0
  36. auditops-0.1.0/src/auditops/providers/aws/tests/ebs/volume_tags.py +52 -0
  37. auditops-0.1.0/src/auditops/providers/aws/tests/ec2/__init__.py +5 -0
  38. auditops-0.1.0/src/auditops/providers/aws/tests/ec2/instance_tags.py +52 -0
  39. auditops-0.1.0/src/auditops/providers/aws/tests/ec2/security_group_tags.py +51 -0
  40. auditops-0.1.0/src/auditops/providers/aws/tests/guardduty/__init__.py +3 -0
  41. auditops-0.1.0/src/auditops/providers/aws/tests/guardduty/enabled.py +61 -0
  42. auditops-0.1.0/src/auditops/providers/aws/tests/iam/__init__.py +7 -0
  43. auditops-0.1.0/src/auditops/providers/aws/tests/iam/password_policy.py +76 -0
  44. auditops-0.1.0/src/auditops/providers/aws/tests/iam/root_access_key.py +31 -0
  45. auditops-0.1.0/src/auditops/providers/aws/tests/iam/root_mfa.py +29 -0
  46. auditops-0.1.0/src/auditops/providers/aws/tests/iam/user_access_key_age.py +80 -0
  47. auditops-0.1.0/src/auditops/providers/aws/tests/iam/user_mfa.py +60 -0
  48. auditops-0.1.0/src/auditops/providers/aws/tests/lmbda/__init__.py +3 -0
  49. auditops-0.1.0/src/auditops/providers/aws/tests/lmbda/tags.py +56 -0
  50. auditops-0.1.0/src/auditops/providers/aws/tests/rds/__init__.py +9 -0
  51. auditops-0.1.0/src/auditops/providers/aws/tests/rds/auto_minor_version_upgrade.py +45 -0
  52. auditops-0.1.0/src/auditops/providers/aws/tests/rds/backup_retention.py +48 -0
  53. auditops-0.1.0/src/auditops/providers/aws/tests/rds/deletion_protection.py +68 -0
  54. auditops-0.1.0/src/auditops/providers/aws/tests/rds/encryption.py +45 -0
  55. auditops-0.1.0/src/auditops/providers/aws/tests/rds/public_access.py +46 -0
  56. auditops-0.1.0/src/auditops/providers/aws/tests/rds/tags.py +51 -0
  57. auditops-0.1.0/src/auditops/providers/aws/tests/s3/__init__.py +7 -0
  58. auditops-0.1.0/src/auditops/providers/aws/tests/s3/encryption.py +51 -0
  59. auditops-0.1.0/src/auditops/providers/aws/tests/s3/public_access.py +54 -0
  60. auditops-0.1.0/src/auditops/providers/aws/tests/s3/secure_transport.py +71 -0
  61. auditops-0.1.0/src/auditops/providers/aws/tests/s3/tags.py +59 -0
  62. auditops-0.1.0/src/auditops/providers/github/__init__.py +4 -0
  63. auditops-0.1.0/src/auditops/providers/github/github_collector.py +62 -0
  64. auditops-0.1.0/src/auditops/providers/github/github_tester.py +33 -0
  65. auditops-0.1.0/src/auditops/providers/github/tests/org.py +57 -0
  66. auditops-0.1.0/src/auditops/providers/github/tests/repos.py +37 -0
  67. auditops-0.1.0/src/auditops/providers/google_workspace/__init__.py +3 -0
  68. auditops-0.1.0/src/auditops/providers/google_workspace/google_workspace_collector.py +28 -0
  69. auditops-0.1.0/src/auditops.egg-info/PKG-INFO +41 -0
  70. auditops-0.1.0/src/auditops.egg-info/SOURCES.txt +71 -0
  71. auditops-0.1.0/src/auditops.egg-info/dependency_links.txt +1 -0
  72. auditops-0.1.0/src/auditops.egg-info/requires.txt +3 -0
  73. 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.
@@ -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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )