vigilai-scan 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VigilAI
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,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: vigilai-scan
3
+ Version: 0.1.0
4
+ Summary: Read-only AWS compliance baseline scanner — 15 checks mapped to SOC 2 criteria, with the Terraform fix for every failure
5
+ Author: VigilAI
6
+ License: MIT
7
+ Keywords: aws,compliance,soc2,security,scanner
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: boto3>=1.28
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7; extra == "dev"
14
+ Dynamic: license-file
15
+
16
+ # vigilai-scan
17
+
18
+ **Find out in 60 seconds why your AWS account would fail an enterprise security review.**
19
+
20
+ 15 read-only checks against the controls that SOC 2 auditors, enterprise procurement teams, and security questionnaires actually ask about — each mapped to its SOC 2 criterion, each with a concrete fix.
21
+
22
+ ```
23
+ FAIL LOG-01 A multi-region CloudTrail trail is logging [CRITICAL · SOC2 CC7.2]
24
+ FAIL NET-02 No security group opens SSH/RDP to the world [CRITICAL · SOC2 CC6.6]
25
+ SG(s) with 0.0.0.0/0 on admin ports: sg-0892…, sg-0b37…
26
+ FAIL S3-01 Account-level S3 public access block is on [CRITICAL · SOC2 CC6.6]
27
+ ...
28
+ 7 passed · 7 failed · 1 warnings
29
+ ```
30
+
31
+ ## Run it
32
+
33
+ ```bash
34
+ pip install vigilai-scan # or: pipx run vigilai-scan
35
+ vigilai-scan # uses your default AWS profile
36
+ vigilai-scan --profile prod --region us-west-2
37
+ vigilai-scan --json > report.json # CI-friendly; exits 1 on any FAIL
38
+ ```
39
+
40
+ **Read-only, local, zero telemetry.** Every AWS call is a `Describe`/`List`/`Get`. Nothing is modified, nothing leaves your machine. The safest way to run it is with the AWS-managed `SecurityAudit` policy. Audit the ~700 lines of source yourself — that's the point of it being open.
41
+
42
+ ## What it checks
43
+
44
+ | ID | Check | SOC 2 |
45
+ |---|---|---|
46
+ | IAM-01 | Root account MFA | CC6.1 |
47
+ | IAM-02 | No IAM users with console passwords (SSO roles instead) | CC6.2 |
48
+ | IAM-03 | MFA on every human identity | CC6.1 |
49
+ | IAM-04 | No access keys older than 90 days | CC6.2 |
50
+ | IAM-05 | No `AdministratorAccess` attached directly to users | CC6.3 |
51
+ | S3-01 | Account-level public access block | CC6.6 |
52
+ | S3-02 | Default encryption on every bucket | CC6.1 |
53
+ | S3-03 | Per-bucket public access blocks | CC6.6 |
54
+ | NET-01 | Default security groups deny all traffic | CC6.6 |
55
+ | NET-02 | No SSH/RDP open to 0.0.0.0/0 | CC6.6 |
56
+ | NET-03 | VPC flow logs enabled | CC7.3 |
57
+ | NET-04 | Peering routes scoped tighter than /24 | CC6.6 |
58
+ | KMS-01 | Rotation on customer-managed keys | CC6.1 |
59
+ | LOG-01 | Active multi-region CloudTrail | CC7.2 |
60
+ | LOG-02 | EBS encryption-by-default | CC6.1 |
61
+
62
+ ## The full baseline checklist
63
+
64
+ The scanner covers what's machine-checkable. The complete ~30-control checklist — including the process controls (access reviews, IR severity levels, vuln SLAs, change management) — is in [CHECKLIST.md](CHECKLIST.md). It's the engineering half of compliance, free under CC BY 4.0.
65
+
66
+ ## The kit
67
+
68
+ Every failing check above has a ready-to-apply fix — a Terraform module or hardened template, plus the five corporate policies (Access Control, Change Management, Data Classification, Incident Response, Vulnerability Management) that enterprise reviewers ask for, pre-mapped to the code that enforces them.
69
+
70
+ > **⚡ Compliance Kit — $349 one-time.** Versioned Terraform modules + policies + control-to-code mapping, delivered as a private git repo. [Get it here.](https://YOUR-CHECKOUT-LINK)
71
+ >
72
+ > **🚀 Compliance Sprint — done with you in 14 days.** We run the scan, deploy the fixes into your account, customize the policies, and hand you a filled-out security questionnaire answer bank. [Book a free scan review.](https://calendly.com/banhjosh/30min)
73
+
74
+ ## FAQ
75
+
76
+ **Why not just use Prowler / ScoutSuite?** They're excellent and far more exhaustive — hundreds of findings across every service. This is deliberately the opposite: the 15 checks an enterprise security reviewer will actually probe first, with a fix attached to each. Run this to get deal-ready this week; run Prowler when you have a security team.
77
+
78
+ **Will this get me SOC 2 certified?** No tool does. This gets your technical controls to the baseline auditors expect, which is most of the engineering work and the part that blocks deals.
79
+
80
+ **License:** MIT (scanner), CC BY 4.0 (checklist).
@@ -0,0 +1,65 @@
1
+ # vigilai-scan
2
+
3
+ **Find out in 60 seconds why your AWS account would fail an enterprise security review.**
4
+
5
+ 15 read-only checks against the controls that SOC 2 auditors, enterprise procurement teams, and security questionnaires actually ask about — each mapped to its SOC 2 criterion, each with a concrete fix.
6
+
7
+ ```
8
+ FAIL LOG-01 A multi-region CloudTrail trail is logging [CRITICAL · SOC2 CC7.2]
9
+ FAIL NET-02 No security group opens SSH/RDP to the world [CRITICAL · SOC2 CC6.6]
10
+ SG(s) with 0.0.0.0/0 on admin ports: sg-0892…, sg-0b37…
11
+ FAIL S3-01 Account-level S3 public access block is on [CRITICAL · SOC2 CC6.6]
12
+ ...
13
+ 7 passed · 7 failed · 1 warnings
14
+ ```
15
+
16
+ ## Run it
17
+
18
+ ```bash
19
+ pip install vigilai-scan # or: pipx run vigilai-scan
20
+ vigilai-scan # uses your default AWS profile
21
+ vigilai-scan --profile prod --region us-west-2
22
+ vigilai-scan --json > report.json # CI-friendly; exits 1 on any FAIL
23
+ ```
24
+
25
+ **Read-only, local, zero telemetry.** Every AWS call is a `Describe`/`List`/`Get`. Nothing is modified, nothing leaves your machine. The safest way to run it is with the AWS-managed `SecurityAudit` policy. Audit the ~700 lines of source yourself — that's the point of it being open.
26
+
27
+ ## What it checks
28
+
29
+ | ID | Check | SOC 2 |
30
+ |---|---|---|
31
+ | IAM-01 | Root account MFA | CC6.1 |
32
+ | IAM-02 | No IAM users with console passwords (SSO roles instead) | CC6.2 |
33
+ | IAM-03 | MFA on every human identity | CC6.1 |
34
+ | IAM-04 | No access keys older than 90 days | CC6.2 |
35
+ | IAM-05 | No `AdministratorAccess` attached directly to users | CC6.3 |
36
+ | S3-01 | Account-level public access block | CC6.6 |
37
+ | S3-02 | Default encryption on every bucket | CC6.1 |
38
+ | S3-03 | Per-bucket public access blocks | CC6.6 |
39
+ | NET-01 | Default security groups deny all traffic | CC6.6 |
40
+ | NET-02 | No SSH/RDP open to 0.0.0.0/0 | CC6.6 |
41
+ | NET-03 | VPC flow logs enabled | CC7.3 |
42
+ | NET-04 | Peering routes scoped tighter than /24 | CC6.6 |
43
+ | KMS-01 | Rotation on customer-managed keys | CC6.1 |
44
+ | LOG-01 | Active multi-region CloudTrail | CC7.2 |
45
+ | LOG-02 | EBS encryption-by-default | CC6.1 |
46
+
47
+ ## The full baseline checklist
48
+
49
+ The scanner covers what's machine-checkable. The complete ~30-control checklist — including the process controls (access reviews, IR severity levels, vuln SLAs, change management) — is in [CHECKLIST.md](CHECKLIST.md). It's the engineering half of compliance, free under CC BY 4.0.
50
+
51
+ ## The kit
52
+
53
+ Every failing check above has a ready-to-apply fix — a Terraform module or hardened template, plus the five corporate policies (Access Control, Change Management, Data Classification, Incident Response, Vulnerability Management) that enterprise reviewers ask for, pre-mapped to the code that enforces them.
54
+
55
+ > **⚡ Compliance Kit — $349 one-time.** Versioned Terraform modules + policies + control-to-code mapping, delivered as a private git repo. [Get it here.](https://YOUR-CHECKOUT-LINK)
56
+ >
57
+ > **🚀 Compliance Sprint — done with you in 14 days.** We run the scan, deploy the fixes into your account, customize the policies, and hand you a filled-out security questionnaire answer bank. [Book a free scan review.](https://calendly.com/banhjosh/30min)
58
+
59
+ ## FAQ
60
+
61
+ **Why not just use Prowler / ScoutSuite?** They're excellent and far more exhaustive — hundreds of findings across every service. This is deliberately the opposite: the 15 checks an enterprise security reviewer will actually probe first, with a fix attached to each. Run this to get deal-ready this week; run Prowler when you have a security team.
62
+
63
+ **Will this get me SOC 2 certified?** No tool does. This gets your technical controls to the baseline auditors expect, which is most of the engineering work and the part that blocks deals.
64
+
65
+ **License:** MIT (scanner), CC BY 4.0 (checklist).
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vigilai-scan"
7
+ version = "0.1.0"
8
+ description = "Read-only AWS compliance baseline scanner — 15 checks mapped to SOC 2 criteria, with the Terraform fix for every failure"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "VigilAI" }]
13
+ keywords = ["aws", "compliance", "soc2", "security", "scanner"]
14
+ dependencies = ["boto3>=1.28"]
15
+
16
+ [project.optional-dependencies]
17
+ dev = ["pytest>=7"]
18
+
19
+ [project.scripts]
20
+ vigilai-scan = "vigilai_scan.cli:main"
21
+
22
+ [tool.setuptools.packages.find]
23
+ include = ["vigilai_scan*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,151 @@
1
+ """Unit tests for vigilai-scan checks using stubbed boto3 clients."""
2
+
3
+ import datetime
4
+ from unittest import mock
5
+
6
+ import boto3
7
+ import pytest
8
+ from botocore.stub import Stubber
9
+
10
+ from vigilai_scan import checks # noqa: F401 (registers checks)
11
+ from vigilai_scan.checks import iam as iam_checks
12
+ from vigilai_scan.checks import network as network_checks
13
+ from vigilai_scan.checks import logging_monitoring as log_checks
14
+ from vigilai_scan.registry import all_checks, PASS, FAIL, WARN, ERROR
15
+
16
+
17
+ CRED_REPORT_HEADER = (
18
+ "user,arn,user_creation_time,password_enabled,password_last_used,"
19
+ "password_last_changed,password_next_rotation,mfa_active,"
20
+ "access_key_1_active,access_key_1_last_rotated,"
21
+ "access_key_2_active,access_key_2_last_rotated"
22
+ )
23
+
24
+
25
+ def _session_with_stub(service, region="us-east-1"):
26
+ session = boto3.Session(
27
+ aws_access_key_id="testing", aws_secret_access_key="testing", region_name=region)
28
+ client = session.client(service, region_name=region)
29
+ stubber = Stubber(client)
30
+ patcher = mock.patch.object(session, "client", return_value=client)
31
+ return session, stubber, patcher
32
+
33
+
34
+ def _cred_report_session(rows):
35
+ body = CRED_REPORT_HEADER + "\n" + "\n".join(rows)
36
+ session, stubber, patcher = _session_with_stub("iam")
37
+ stubber.add_response("get_credential_report", {
38
+ "Content": body.encode(), "ReportFormat": "text/csv",
39
+ "GeneratedTime": datetime.datetime(2026, 7, 10),
40
+ })
41
+ return session, stubber, patcher
42
+
43
+
44
+ def test_root_mfa_fail():
45
+ session, stubber, patcher = _cred_report_session([
46
+ "<root_account>,arn:aws:iam::1:root,2020-01-01T00:00:00Z,true,N/A,N/A,N/A,false,false,N/A,false,N/A",
47
+ ])
48
+ with stubber, patcher:
49
+ result = iam_checks.root_mfa(session)[0]
50
+ assert result.status == FAIL
51
+ assert result.check_id == "IAM-01"
52
+
53
+
54
+ def test_root_mfa_pass():
55
+ session, stubber, patcher = _cred_report_session([
56
+ "<root_account>,arn:aws:iam::1:root,2020-01-01T00:00:00Z,true,N/A,N/A,N/A,true,false,N/A,false,N/A",
57
+ ])
58
+ with stubber, patcher:
59
+ result = iam_checks.root_mfa(session)[0]
60
+ assert result.status == PASS
61
+
62
+
63
+ def test_console_users_flags_password_users():
64
+ session, stubber, patcher = _cred_report_session([
65
+ "<root_account>,arn,2020-01-01T00:00:00Z,true,N/A,N/A,N/A,true,false,N/A,false,N/A",
66
+ "alice,arn,2021-01-01T00:00:00Z,true,N/A,N/A,N/A,true,false,N/A,false,N/A",
67
+ "ci-bot,arn,2021-01-01T00:00:00Z,false,N/A,N/A,N/A,false,true,2026-07-01T00:00:00Z,false,N/A",
68
+ ])
69
+ with stubber, patcher:
70
+ result = iam_checks.console_users(session)[0]
71
+ assert result.status == FAIL
72
+ assert "alice" in result.detail
73
+ assert "ci-bot" not in result.detail
74
+
75
+
76
+ def test_stale_access_keys():
77
+ session, stubber, patcher = _cred_report_session([
78
+ "ci-bot,arn,2021-01-01T00:00:00Z,false,N/A,N/A,N/A,false,true,2020-01-01T00:00:00+00:00,false,N/A",
79
+ ])
80
+ with stubber, patcher:
81
+ result = iam_checks.stale_access_keys(session)[0]
82
+ assert result.status == FAIL
83
+ assert "ci-bot" in result.detail
84
+
85
+
86
+ def test_default_sg_locked_pass_and_fail():
87
+ session, stubber, patcher = _session_with_stub("ec2")
88
+ stubber.add_response("describe_security_groups", {"SecurityGroups": [
89
+ {"GroupId": "sg-clean", "GroupName": "default", "OwnerId": "1",
90
+ "Description": "d", "IpPermissions": [], "IpPermissionsEgress": []},
91
+ ]})
92
+ with stubber, patcher:
93
+ assert network_checks.default_sg_locked(session)[0].status == PASS
94
+
95
+ session, stubber, patcher = _session_with_stub("ec2")
96
+ stubber.add_response("describe_security_groups", {"SecurityGroups": [
97
+ {"GroupId": "sg-open", "GroupName": "default", "OwnerId": "1", "Description": "d",
98
+ "IpPermissions": [{"IpProtocol": "-1", "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}],
99
+ "IpPermissionsEgress": []},
100
+ ]})
101
+ with stubber, patcher:
102
+ result = network_checks.default_sg_locked(session)[0]
103
+ assert result.status == FAIL
104
+ assert "sg-open" in result.detail
105
+
106
+
107
+ def test_world_open_admin_ports():
108
+ session, stubber, patcher = _session_with_stub("ec2")
109
+ stubber.add_response("describe_security_groups", {"SecurityGroups": [
110
+ {"GroupId": "sg-ssh", "GroupName": "web", "OwnerId": "1", "Description": "d",
111
+ "IpPermissions": [{"IpProtocol": "tcp", "FromPort": 22, "ToPort": 22,
112
+ "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}]},
113
+ {"GroupId": "sg-https", "GroupName": "web2", "OwnerId": "1", "Description": "d",
114
+ "IpPermissions": [{"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443,
115
+ "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}]},
116
+ ]})
117
+ with stubber, patcher:
118
+ result = network_checks.world_open_admin_ports(session)[0]
119
+ assert result.status == FAIL
120
+ assert "sg-ssh" in result.detail
121
+ assert "sg-https" not in result.detail
122
+
123
+
124
+ def test_broad_peering_route_flagged():
125
+ session, stubber, patcher = _session_with_stub("ec2")
126
+ stubber.add_response("describe_route_tables", {"RouteTables": [
127
+ {"RouteTableId": "rtb-1", "Routes": [
128
+ {"DestinationCidrBlock": "10.1.0.0/16", "VpcPeeringConnectionId": "pcx-1"},
129
+ {"DestinationCidrBlock": "10.2.5.0/24", "VpcPeeringConnectionId": "pcx-2"},
130
+ {"DestinationCidrBlock": "0.0.0.0/0", "GatewayId": "igw-1"},
131
+ ]},
132
+ ]})
133
+ with stubber, patcher:
134
+ result = network_checks.broad_peering_routes(session)[0]
135
+ assert result.status == FAIL
136
+ assert "10.1.0.0/16" in result.detail
137
+ assert "10.2.5.0/24" not in result.detail
138
+
139
+
140
+ def test_access_denied_becomes_skip():
141
+ session, stubber, patcher = _session_with_stub("ec2")
142
+ stubber.add_client_error("get_ebs_encryption_by_default",
143
+ service_error_code="UnauthorizedOperation")
144
+ with stubber, patcher:
145
+ result = log_checks.ebs_default_encryption(session)[0]
146
+ assert result.status == ERROR
147
+ assert "SecurityAudit" in result.detail or "AWS error" in result.detail
148
+
149
+
150
+ def test_registry_has_15_checks():
151
+ assert len(all_checks()) == 15
@@ -0,0 +1,3 @@
1
+ """vigilai-scan: read-only AWS compliance baseline scanner."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ """Import all check modules so their @guard decorators register them."""
2
+
3
+ from . import iam, s3, network, kms, logging_monitoring # noqa: F401
@@ -0,0 +1,111 @@
1
+ """IAM checks — all built on the credential report plus two List calls."""
2
+
3
+ import csv
4
+ import io
5
+ import time
6
+ from datetime import datetime, timedelta, timezone
7
+
8
+ from ..registry import CheckResult, guard, PASS, FAIL, WARN
9
+
10
+ KEY_MAX_AGE_DAYS = 90
11
+
12
+
13
+ def _credential_report(session) -> list:
14
+ """Fetch (generating if needed) the IAM credential report as dict rows."""
15
+ iam = session.client("iam")
16
+ for _ in range(10):
17
+ try:
18
+ report = iam.get_credential_report()
19
+ break
20
+ except iam.exceptions.CredentialReportNotPresentException:
21
+ iam.generate_credential_report()
22
+ time.sleep(2)
23
+ else:
24
+ raise RuntimeError("credential report never became available")
25
+ body = report["Content"].decode("utf-8")
26
+ return list(csv.DictReader(io.StringIO(body)))
27
+
28
+
29
+ @guard("IAM-01", "Root account has MFA enabled", "CRITICAL", "CC6.1")
30
+ def root_mfa(session, region=None):
31
+ rows = _credential_report(session)
32
+ root = next((r for r in rows if r["user"] == "<root_account>"), None)
33
+ ok = root is not None and root.get("mfa_active") == "true"
34
+ return [CheckResult(
35
+ "IAM-01", "Root account has MFA enabled", "CRITICAL", "CC6.1",
36
+ PASS if ok else FAIL,
37
+ "" if ok else "The root account can be taken over with a single password.",
38
+ fix="checklist: Access Control — MFA enforced on console",
39
+ )]
40
+
41
+
42
+ @guard("IAM-02", "No IAM users with console passwords (use SSO roles)", "HIGH", "CC6.2")
43
+ def console_users(session, region=None):
44
+ rows = _credential_report(session)
45
+ with_pw = [r["user"] for r in rows
46
+ if r["user"] != "<root_account>" and r.get("password_enabled") == "true"]
47
+ ok = not with_pw
48
+ return [CheckResult(
49
+ "IAM-02", "No IAM users with console passwords (use SSO roles)", "HIGH", "CC6.2",
50
+ PASS if ok else FAIL,
51
+ "" if ok else f"{len(with_pw)} IAM user(s) with passwords: {', '.join(with_pw[:5])}"
52
+ + ("…" if len(with_pw) > 5 else ""),
53
+ fix="kit: terraform/modules/iam-least-privilege (SSO-federated roles)",
54
+ )]
55
+
56
+
57
+ @guard("IAM-03", "Every human identity has MFA", "HIGH", "CC6.1")
58
+ def users_without_mfa(session, region=None):
59
+ rows = _credential_report(session)
60
+ no_mfa = [r["user"] for r in rows
61
+ if r["user"] != "<root_account>"
62
+ and r.get("password_enabled") == "true"
63
+ and r.get("mfa_active") != "true"]
64
+ ok = not no_mfa
65
+ return [CheckResult(
66
+ "IAM-03", "Every human identity has MFA", "HIGH", "CC6.1",
67
+ PASS if ok else FAIL,
68
+ "" if ok else f"{len(no_mfa)} console user(s) without MFA: {', '.join(no_mfa[:5])}"
69
+ + ("…" if len(no_mfa) > 5 else ""),
70
+ fix="checklist: Access Control — MFA enforced on console, IdP, and source control",
71
+ )]
72
+
73
+
74
+ @guard("IAM-04", f"No access keys older than {KEY_MAX_AGE_DAYS} days", "MEDIUM", "CC6.2")
75
+ def stale_access_keys(session, region=None):
76
+ rows = _credential_report(session)
77
+ cutoff = datetime.now(timezone.utc) - timedelta(days=KEY_MAX_AGE_DAYS)
78
+ stale = []
79
+ for r in rows:
80
+ for slot in ("1", "2"):
81
+ if r.get(f"access_key_{slot}_active") == "true":
82
+ rotated = r.get(f"access_key_{slot}_last_rotated", "")
83
+ if rotated and rotated not in ("N/A", "not_supported"):
84
+ when = datetime.fromisoformat(rotated.replace("Z", "+00:00"))
85
+ if when < cutoff:
86
+ stale.append(f"{r['user']} (key {slot})")
87
+ ok = not stale
88
+ return [CheckResult(
89
+ "IAM-04", f"No access keys older than {KEY_MAX_AGE_DAYS} days", "MEDIUM", "CC6.2",
90
+ PASS if ok else FAIL,
91
+ "" if ok else f"{len(stale)} stale key(s): {', '.join(stale[:5])}"
92
+ + ("…" if len(stale) > 5 else ""),
93
+ fix="kit: terraform/modules/iam-least-privilege (OIDC short-lived credentials for CI)",
94
+ )]
95
+
96
+
97
+ @guard("IAM-05", "AdministratorAccess is not attached directly to users", "HIGH", "CC6.3")
98
+ def admin_attached_to_users(session, region=None):
99
+ iam = session.client("iam")
100
+ entities = iam.list_entities_for_policy(
101
+ PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess",
102
+ EntityFilter="User",
103
+ )
104
+ users = [u["UserName"] for u in entities.get("PolicyUsers", [])]
105
+ ok = not users
106
+ return [CheckResult(
107
+ "IAM-05", "AdministratorAccess is not attached directly to users", "HIGH", "CC6.3",
108
+ PASS if ok else FAIL,
109
+ "" if ok else f"Admin users: {', '.join(users[:5])}" + ("…" if len(users) > 5 else ""),
110
+ fix="kit: terraform/modules/iam-least-privilege (MFA-gated break-glass role instead)",
111
+ )]
@@ -0,0 +1,35 @@
1
+ """KMS checks: rotation on customer-managed keys."""
2
+
3
+ from ..registry import CheckResult, guard, PASS, FAIL, WARN
4
+
5
+
6
+ @guard("KMS-01", "Customer-managed keys have rotation enabled", "MEDIUM", "CC6.1")
7
+ def key_rotation(session, region=None):
8
+ kms = session.client("kms", region_name=region)
9
+ unrotated, checked = [], 0
10
+ paginator = kms.get_paginator("list_keys")
11
+ for page in paginator.paginate():
12
+ for key in page.get("Keys", []):
13
+ meta = kms.describe_key(KeyId=key["KeyId"])["KeyMetadata"]
14
+ if meta.get("KeyManager") != "CUSTOMER" or meta.get("KeyState") != "Enabled":
15
+ continue
16
+ if meta.get("KeySpec", "SYMMETRIC_DEFAULT") != "SYMMETRIC_DEFAULT":
17
+ continue # rotation only applies to symmetric keys
18
+ checked += 1
19
+ status = kms.get_key_rotation_status(KeyId=key["KeyId"])
20
+ if not status.get("KeyRotationEnabled"):
21
+ unrotated.append(key["KeyId"])
22
+ if checked == 0:
23
+ return [CheckResult(
24
+ "KMS-01", "Customer-managed keys have rotation enabled", "MEDIUM", "CC6.1",
25
+ WARN, "No customer-managed keys found — are you encrypting with CMKs at all?",
26
+ fix="kit: terraform/modules/kms-baseline",
27
+ )]
28
+ ok = not unrotated
29
+ return [CheckResult(
30
+ "KMS-01", "Customer-managed keys have rotation enabled", "MEDIUM", "CC6.1",
31
+ PASS if ok else FAIL,
32
+ "" if ok else f"{len(unrotated)} key(s) without rotation: {', '.join(unrotated[:3])}"
33
+ + ("…" if len(unrotated) > 3 else ""),
34
+ fix="kit: terraform/modules/kms-baseline (enable_key_rotation = true)",
35
+ )]
@@ -0,0 +1,35 @@
1
+ """Logging & monitoring checks: CloudTrail coverage, EBS default encryption."""
2
+
3
+ from ..registry import CheckResult, guard, PASS, FAIL, WARN
4
+
5
+
6
+ @guard("LOG-01", "A multi-region CloudTrail trail is logging", "CRITICAL", "CC7.2")
7
+ def cloudtrail_active(session, region=None):
8
+ ct = session.client("cloudtrail", region_name=region)
9
+ trails = ct.describe_trails(includeShadowTrails=True).get("trailList", [])
10
+ active_multiregion = []
11
+ for t in trails:
12
+ if not t.get("IsMultiRegionTrail"):
13
+ continue
14
+ status = ct.get_trail_status(Name=t["TrailARN"])
15
+ if status.get("IsLogging"):
16
+ active_multiregion.append(t["Name"])
17
+ ok = bool(active_multiregion)
18
+ return [CheckResult(
19
+ "LOG-01", "A multi-region CloudTrail trail is logging", "CRITICAL", "CC7.2",
20
+ PASS if ok else FAIL,
21
+ "" if ok else "No active multi-region trail — you cannot answer 'who did what' during an incident.",
22
+ fix="checklist: Incident Response — evidence collection before you need it",
23
+ )]
24
+
25
+
26
+ @guard("LOG-02", "EBS encryption-by-default is on", "MEDIUM", "CC6.1")
27
+ def ebs_default_encryption(session, region=None):
28
+ ec2 = session.client("ec2", region_name=region)
29
+ enabled = ec2.get_ebs_encryption_by_default().get("EbsEncryptionByDefault", False)
30
+ return [CheckResult(
31
+ "LOG-02", "EBS encryption-by-default is on", "MEDIUM", "CC6.1",
32
+ PASS if enabled else FAIL,
33
+ "" if enabled else "New volumes are created unencrypted unless every engineer remembers the flag.",
34
+ fix="checklist: Encryption — one API call fixes this account-wide",
35
+ )]
@@ -0,0 +1,94 @@
1
+ """Network checks: default SGs, world-open admin ports, flow logs, broad peering routes."""
2
+
3
+ import ipaddress
4
+
5
+ from ..registry import CheckResult, guard, PASS, FAIL, WARN
6
+
7
+ ADMIN_PORTS = {22, 3389}
8
+ PEER_MAX_PREFIX = 24 # peering routes broader than /24 are flagged
9
+
10
+
11
+ @guard("NET-01", "Default security groups allow no traffic", "HIGH", "CC6.6")
12
+ def default_sg_locked(session, region=None):
13
+ ec2 = session.client("ec2", region_name=region)
14
+ groups = ec2.describe_security_groups(
15
+ Filters=[{"Name": "group-name", "Values": ["default"]}]).get("SecurityGroups", [])
16
+ open_defaults = [g["GroupId"] for g in groups
17
+ if g.get("IpPermissions") or g.get("IpPermissionsEgress")]
18
+ ok = not open_defaults
19
+ return [CheckResult(
20
+ "NET-01", "Default security groups allow no traffic", "HIGH", "CC6.6",
21
+ PASS if ok else FAIL,
22
+ "" if ok else f"Default SG(s) with rules: {', '.join(open_defaults[:5])}"
23
+ + ("…" if len(open_defaults) > 5 else ""),
24
+ fix="kit: terraform/modules/vpc-baseline (aws_default_security_group with no rules)",
25
+ )]
26
+
27
+
28
+ def _rule_hits_admin_port(perm) -> bool:
29
+ if perm.get("IpProtocol") == "-1":
30
+ return True
31
+ from_p, to_p = perm.get("FromPort"), perm.get("ToPort")
32
+ if from_p is None or to_p is None:
33
+ return False
34
+ return any(from_p <= p <= to_p for p in ADMIN_PORTS)
35
+
36
+
37
+ @guard("NET-02", "No security group opens SSH/RDP to the world", "CRITICAL", "CC6.6")
38
+ def world_open_admin_ports(session, region=None):
39
+ ec2 = session.client("ec2", region_name=region)
40
+ offenders = []
41
+ paginator = ec2.get_paginator("describe_security_groups")
42
+ for page in paginator.paginate():
43
+ for g in page.get("SecurityGroups", []):
44
+ for perm in g.get("IpPermissions", []):
45
+ world = any(r.get("CidrIp") == "0.0.0.0/0" for r in perm.get("IpRanges", [])) or \
46
+ any(r.get("CidrIpv6") == "::/0" for r in perm.get("Ipv6Ranges", []))
47
+ if world and _rule_hits_admin_port(perm):
48
+ offenders.append(g["GroupId"])
49
+ break
50
+ ok = not offenders
51
+ return [CheckResult(
52
+ "NET-02", "No security group opens SSH/RDP to the world", "CRITICAL", "CC6.6",
53
+ PASS if ok else FAIL,
54
+ "" if ok else f"SG(s) with 0.0.0.0/0 on admin ports: {', '.join(offenders[:5])}"
55
+ + ("…" if len(offenders) > 5 else ""),
56
+ fix="checklist: Network — no world-open admin ports; use SSM Session Manager",
57
+ )]
58
+
59
+
60
+ @guard("NET-03", "Every VPC has flow logs enabled", "MEDIUM", "CC7.3")
61
+ def vpc_flow_logs(session, region=None):
62
+ ec2 = session.client("ec2", region_name=region)
63
+ vpcs = {v["VpcId"] for v in ec2.describe_vpcs().get("Vpcs", [])}
64
+ logged = {f["ResourceId"] for f in ec2.describe_flow_logs().get("FlowLogs", [])}
65
+ missing = sorted(vpcs - logged)
66
+ ok = not missing
67
+ return [CheckResult(
68
+ "NET-03", "Every VPC has flow logs enabled", "MEDIUM", "CC7.3",
69
+ PASS if ok else FAIL,
70
+ "" if ok else f"VPC(s) without flow logs: {', '.join(missing[:5])}"
71
+ + ("…" if len(missing) > 5 else ""),
72
+ fix="kit: terraform/modules/vpc-baseline (aws_flow_log, 90-day retention)",
73
+ )]
74
+
75
+
76
+ @guard("NET-04", f"Peering routes are scoped tighter than /{PEER_MAX_PREFIX}", "HIGH", "CC6.6")
77
+ def broad_peering_routes(session, region=None):
78
+ ec2 = session.client("ec2", region_name=region)
79
+ broad = []
80
+ for table in ec2.describe_route_tables().get("RouteTables", []):
81
+ for route in table.get("Routes", []):
82
+ if route.get("VpcPeeringConnectionId") and route.get("DestinationCidrBlock"):
83
+ net = ipaddress.ip_network(route["DestinationCidrBlock"], strict=False)
84
+ if net.prefixlen < PEER_MAX_PREFIX:
85
+ broad.append(f"{table['RouteTableId']}→{route['DestinationCidrBlock']}")
86
+ ok = not broad
87
+ return [CheckResult(
88
+ "NET-04", f"Peering routes are scoped tighter than /{PEER_MAX_PREFIX}", "HIGH", "CC6.6",
89
+ PASS if ok else FAIL,
90
+ "" if ok else f"Broad peering route(s): {', '.join(broad[:5])}"
91
+ + ("…" if len(broad) > 5 else "")
92
+ + " — a compromised peer network can reach everything in these ranges.",
93
+ fix="kit: terraform/modules/vpc-baseline (scoped peer routes, none to data subnets)",
94
+ )]
@@ -0,0 +1,86 @@
1
+ """S3 checks: account-level public access block, per-bucket encryption and PAB."""
2
+
3
+ from botocore.exceptions import ClientError
4
+
5
+ from ..registry import CheckResult, guard, PASS, FAIL, WARN
6
+
7
+ MAX_BUCKETS = 200 # keep scans fast on large accounts
8
+
9
+
10
+ @guard("S3-01", "Account-level S3 public access block is fully on", "CRITICAL", "CC6.6")
11
+ def account_public_access_block(session, region=None):
12
+ sts = session.client("sts")
13
+ account_id = sts.get_caller_identity()["Account"]
14
+ s3control = session.client("s3control", region_name=region or "us-east-1")
15
+ try:
16
+ cfg = s3control.get_public_access_block(AccountId=account_id)[
17
+ "PublicAccessBlockConfiguration"]
18
+ ok = all(cfg.get(k) for k in (
19
+ "BlockPublicAcls", "IgnorePublicAcls", "BlockPublicPolicy", "RestrictPublicBuckets"))
20
+ detail = "" if ok else "One or more account-level public access settings are off."
21
+ except ClientError as exc:
22
+ if exc.response["Error"]["Code"] == "NoSuchPublicAccessBlockConfiguration":
23
+ ok, detail = False, "No account-level public access block configured at all."
24
+ else:
25
+ raise
26
+ return [CheckResult(
27
+ "S3-01", "Account-level S3 public access block is fully on", "CRITICAL", "CC6.6",
28
+ PASS if ok else FAIL, detail,
29
+ fix="kit: terraform/modules/kms-baseline (bucket hardening pattern)",
30
+ )]
31
+
32
+
33
+ def _bucket_names(session):
34
+ s3 = session.client("s3")
35
+ return [b["Name"] for b in s3.list_buckets().get("Buckets", [])][:MAX_BUCKETS]
36
+
37
+
38
+ @guard("S3-02", "Every bucket has default encryption", "HIGH", "CC6.1")
39
+ def bucket_encryption(session, region=None):
40
+ s3 = session.client("s3")
41
+ unencrypted = []
42
+ for name in _bucket_names(session):
43
+ try:
44
+ s3.get_bucket_encryption(Bucket=name)
45
+ except ClientError as exc:
46
+ if exc.response["Error"]["Code"] == "ServerSideEncryptionConfigurationNotFoundError":
47
+ unencrypted.append(name)
48
+ elif exc.response["Error"]["Code"] in ("AccessDenied", "AccessDeniedException"):
49
+ continue # skip buckets we can't read rather than failing the check
50
+ else:
51
+ raise
52
+ ok = not unencrypted
53
+ return [CheckResult(
54
+ "S3-02", "Every bucket has default encryption", "HIGH", "CC6.1",
55
+ PASS if ok else FAIL,
56
+ "" if ok else f"{len(unencrypted)} unencrypted bucket(s): {', '.join(unencrypted[:5])}"
57
+ + ("…" if len(unencrypted) > 5 else ""),
58
+ fix="kit: terraform/modules/kms-baseline (SSE-KMS with rotation)",
59
+ )]
60
+
61
+
62
+ @guard("S3-03", "Every bucket has its own public access block", "HIGH", "CC6.6")
63
+ def bucket_public_access_block(session, region=None):
64
+ s3 = session.client("s3")
65
+ missing = []
66
+ for name in _bucket_names(session):
67
+ try:
68
+ cfg = s3.get_public_access_block(Bucket=name)["PublicAccessBlockConfiguration"]
69
+ if not all(cfg.get(k) for k in (
70
+ "BlockPublicAcls", "IgnorePublicAcls", "BlockPublicPolicy", "RestrictPublicBuckets")):
71
+ missing.append(name)
72
+ except ClientError as exc:
73
+ if exc.response["Error"]["Code"] == "NoSuchPublicAccessBlockConfiguration":
74
+ missing.append(name)
75
+ elif exc.response["Error"]["Code"] in ("AccessDenied", "AccessDeniedException"):
76
+ continue
77
+ else:
78
+ raise
79
+ ok = not missing
80
+ return [CheckResult(
81
+ "S3-03", "Every bucket has its own public access block", "HIGH", "CC6.6",
82
+ PASS if ok else FAIL,
83
+ "" if ok else f"{len(missing)} bucket(s) without full PAB: {', '.join(missing[:5])}"
84
+ + ("…" if len(missing) > 5 else ""),
85
+ fix="kit: terraform/modules/kms-baseline (aws_s3_bucket_public_access_block)",
86
+ )]
@@ -0,0 +1,60 @@
1
+ """vigilai-scan CLI entry point.
2
+
3
+ Usage:
4
+ vigilai-scan # default profile & region
5
+ vigilai-scan --profile staging --region us-west-2
6
+ vigilai-scan --json > report.json # machine-readable, CI-friendly
7
+
8
+ Exit codes: 0 = all clear, 1 = at least one FAIL, 2 = couldn't run.
9
+ """
10
+
11
+ import argparse
12
+ import sys
13
+
14
+ import boto3
15
+ from botocore.exceptions import NoCredentialsError, ClientError
16
+
17
+ from . import __version__, checks # noqa: F401 (import registers checks)
18
+ from .registry import all_checks, FAIL
19
+ from .report import render_terminal, render_json, supports_color
20
+
21
+
22
+ def main(argv=None) -> int:
23
+ parser = argparse.ArgumentParser(
24
+ prog="vigilai-scan",
25
+ description="Read-only AWS compliance baseline scanner (15 checks, SOC 2 mapped).",
26
+ )
27
+ parser.add_argument("--profile", help="AWS profile name")
28
+ parser.add_argument("--region", help="AWS region (defaults to profile/env region)")
29
+ parser.add_argument("--json", action="store_true", help="emit JSON instead of the terminal report")
30
+ parser.add_argument("--no-color", action="store_true", help="disable ANSI colors")
31
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
32
+ args = parser.parse_args(argv)
33
+
34
+ try:
35
+ session = boto3.Session(profile_name=args.profile, region_name=args.region)
36
+ region = args.region or session.region_name or "us-east-1"
37
+ account = session.client("sts").get_caller_identity()["Account"]
38
+ except NoCredentialsError:
39
+ print("No AWS credentials found. Configure with `aws configure` or set AWS_PROFILE.",
40
+ file=sys.stderr)
41
+ return 2
42
+ except ClientError as exc:
43
+ print(f"Could not authenticate to AWS: {exc}", file=sys.stderr)
44
+ return 2
45
+
46
+ results = []
47
+ for check in all_checks():
48
+ results.extend(check.func(session, region))
49
+
50
+ if args.json:
51
+ print(render_json(results, account, region))
52
+ else:
53
+ color = supports_color() and not args.no_color
54
+ print(render_terminal(results, account, region, color=color))
55
+
56
+ return 1 if any(r.status == FAIL for r in results) else 0
57
+
58
+
59
+ if __name__ == "__main__":
60
+ sys.exit(main())
@@ -0,0 +1,86 @@
1
+ """Check result model and registry.
2
+
3
+ Every check is a function taking a boto3 Session and returning a list of
4
+ CheckResult. Checks make exclusively read-only (Describe/List/Get) calls.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Callable, List, Optional
9
+
10
+ from botocore.exceptions import ClientError, NoCredentialsError
11
+
12
+ PASS = "PASS"
13
+ FAIL = "FAIL"
14
+ WARN = "WARN"
15
+ ERROR = "ERROR" # couldn't evaluate (usually AccessDenied)
16
+
17
+ SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
18
+
19
+
20
+ @dataclass
21
+ class CheckResult:
22
+ check_id: str
23
+ title: str
24
+ severity: str # CRITICAL / HIGH / MEDIUM / LOW
25
+ soc2: str # e.g. "CC6.1"
26
+ status: str # PASS / FAIL / WARN / ERROR
27
+ detail: str = ""
28
+ fix: str = "" # pointer to the remediation in the kit/checklist
29
+
30
+ def to_dict(self) -> dict:
31
+ return {
32
+ "id": self.check_id,
33
+ "title": self.title,
34
+ "severity": self.severity,
35
+ "soc2": self.soc2,
36
+ "status": self.status,
37
+ "detail": self.detail,
38
+ "fix": self.fix,
39
+ }
40
+
41
+
42
+ @dataclass
43
+ class Check:
44
+ func: Callable
45
+ name: str
46
+
47
+
48
+ _REGISTRY: List[Check] = []
49
+
50
+
51
+ def register(func: Callable) -> Callable:
52
+ _REGISTRY.append(Check(func=func, name=func.__name__))
53
+ return func
54
+
55
+
56
+ def all_checks() -> List[Check]:
57
+ return list(_REGISTRY)
58
+
59
+
60
+ def guard(check_id: str, title: str, severity: str, soc2: str):
61
+ """Wrap a check so auth/permission failures become ERROR results
62
+ instead of crashing the whole scan."""
63
+
64
+ def decorator(func: Callable):
65
+ def wrapper(session, region: Optional[str] = None) -> List[CheckResult]:
66
+ try:
67
+ return func(session, region)
68
+ except NoCredentialsError:
69
+ raise # fatal: handled by the CLI with a clear message
70
+ except ClientError as exc:
71
+ code = exc.response.get("Error", {}).get("Code", "")
72
+ if code in ("AccessDenied", "AccessDeniedException", "UnauthorizedOperation", "AuthorizationError"):
73
+ return [CheckResult(
74
+ check_id=check_id, title=title, severity=severity, soc2=soc2,
75
+ status=ERROR,
76
+ detail="Access denied — attach the AWS-managed SecurityAudit policy to the scanning role.",
77
+ )]
78
+ return [CheckResult(
79
+ check_id=check_id, title=title, severity=severity, soc2=soc2,
80
+ status=ERROR, detail=f"AWS error: {code}",
81
+ )]
82
+
83
+ wrapper.__name__ = func.__name__
84
+ return register(wrapper)
85
+
86
+ return decorator
@@ -0,0 +1,90 @@
1
+ """Terminal and JSON rendering for scan results."""
2
+
3
+ import json
4
+ import sys
5
+ from typing import List
6
+
7
+ from .registry import CheckResult, PASS, FAIL, WARN, ERROR, SEVERITY_ORDER
8
+
9
+ RESET = "\033[0m"
10
+ BOLD = "\033[1m"
11
+ DIM = "\033[2m"
12
+ RED = "\033[31m"
13
+ GREEN = "\033[32m"
14
+ YELLOW = "\033[33m"
15
+
16
+ STATUS_STYLE = {
17
+ PASS: (GREEN, "PASS"),
18
+ FAIL: (RED, "FAIL"),
19
+ WARN: (YELLOW, "WARN"),
20
+ ERROR: (DIM, "SKIP"),
21
+ }
22
+
23
+
24
+ def _c(text: str, *styles: str, color: bool = True) -> str:
25
+ if not color:
26
+ return text
27
+ return "".join(styles) + text + RESET
28
+
29
+
30
+ def render_terminal(results: List[CheckResult], account: str, region: str,
31
+ color: bool = True) -> str:
32
+ lines = []
33
+ lines.append("")
34
+ lines.append(_c(" vigilai-scan", BOLD, color=color) + _c(
35
+ f" · account {account} · region {region}", DIM, color=color))
36
+ lines.append(_c(" 15 read-only checks mapped to SOC 2 — nothing is modified or sent anywhere.",
37
+ DIM, color=color))
38
+ lines.append("")
39
+
40
+ ordered = sorted(results, key=lambda r: (
41
+ 0 if r.status == FAIL else 1 if r.status == WARN else 2 if r.status == ERROR else 3,
42
+ SEVERITY_ORDER.get(r.severity, 9), r.check_id))
43
+
44
+ for r in ordered:
45
+ style, label = STATUS_STYLE[r.status]
46
+ head = f" {_c(label, style, BOLD, color=color)} {r.check_id:<7} {r.title}"
47
+ head += _c(f" [{r.severity} · SOC2 {r.soc2}]", DIM, color=color)
48
+ lines.append(head)
49
+ if r.detail and r.status != PASS:
50
+ lines.append(_c(f" {r.detail}", color=color))
51
+ if r.fix and r.status in (FAIL, WARN):
52
+ lines.append(_c(f" fix → {r.fix}", DIM, color=color))
53
+
54
+ fails = sum(1 for r in results if r.status == FAIL)
55
+ warns = sum(1 for r in results if r.status == WARN)
56
+ skips = sum(1 for r in results if r.status == ERROR)
57
+ passes = sum(1 for r in results if r.status == PASS)
58
+
59
+ lines.append("")
60
+ summary = f" {passes} passed · {fails} failed · {warns} warnings"
61
+ if skips:
62
+ summary += f" · {skips} skipped (missing permissions)"
63
+ lines.append(_c(summary, BOLD, color=color))
64
+
65
+ if fails:
66
+ lines.append("")
67
+ lines.append(_c(" Every failing check has a ready-to-apply fix in the compliance kit:",
68
+ color=color))
69
+ lines.append(_c(" https://github.com/Joshugoi/VigilAI#the-kit",
70
+ BOLD, color=color))
71
+ lines.append("")
72
+ return "\n".join(lines)
73
+
74
+
75
+ def render_json(results: List[CheckResult], account: str, region: str) -> str:
76
+ return json.dumps({
77
+ "account": account,
78
+ "region": region,
79
+ "summary": {
80
+ "pass": sum(1 for r in results if r.status == PASS),
81
+ "fail": sum(1 for r in results if r.status == FAIL),
82
+ "warn": sum(1 for r in results if r.status == WARN),
83
+ "skipped": sum(1 for r in results if r.status == ERROR),
84
+ },
85
+ "checks": [r.to_dict() for r in results],
86
+ }, indent=2)
87
+
88
+
89
+ def supports_color(stream=sys.stdout) -> bool:
90
+ return hasattr(stream, "isatty") and stream.isatty()
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: vigilai-scan
3
+ Version: 0.1.0
4
+ Summary: Read-only AWS compliance baseline scanner — 15 checks mapped to SOC 2 criteria, with the Terraform fix for every failure
5
+ Author: VigilAI
6
+ License: MIT
7
+ Keywords: aws,compliance,soc2,security,scanner
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: boto3>=1.28
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7; extra == "dev"
14
+ Dynamic: license-file
15
+
16
+ # vigilai-scan
17
+
18
+ **Find out in 60 seconds why your AWS account would fail an enterprise security review.**
19
+
20
+ 15 read-only checks against the controls that SOC 2 auditors, enterprise procurement teams, and security questionnaires actually ask about — each mapped to its SOC 2 criterion, each with a concrete fix.
21
+
22
+ ```
23
+ FAIL LOG-01 A multi-region CloudTrail trail is logging [CRITICAL · SOC2 CC7.2]
24
+ FAIL NET-02 No security group opens SSH/RDP to the world [CRITICAL · SOC2 CC6.6]
25
+ SG(s) with 0.0.0.0/0 on admin ports: sg-0892…, sg-0b37…
26
+ FAIL S3-01 Account-level S3 public access block is on [CRITICAL · SOC2 CC6.6]
27
+ ...
28
+ 7 passed · 7 failed · 1 warnings
29
+ ```
30
+
31
+ ## Run it
32
+
33
+ ```bash
34
+ pip install vigilai-scan # or: pipx run vigilai-scan
35
+ vigilai-scan # uses your default AWS profile
36
+ vigilai-scan --profile prod --region us-west-2
37
+ vigilai-scan --json > report.json # CI-friendly; exits 1 on any FAIL
38
+ ```
39
+
40
+ **Read-only, local, zero telemetry.** Every AWS call is a `Describe`/`List`/`Get`. Nothing is modified, nothing leaves your machine. The safest way to run it is with the AWS-managed `SecurityAudit` policy. Audit the ~700 lines of source yourself — that's the point of it being open.
41
+
42
+ ## What it checks
43
+
44
+ | ID | Check | SOC 2 |
45
+ |---|---|---|
46
+ | IAM-01 | Root account MFA | CC6.1 |
47
+ | IAM-02 | No IAM users with console passwords (SSO roles instead) | CC6.2 |
48
+ | IAM-03 | MFA on every human identity | CC6.1 |
49
+ | IAM-04 | No access keys older than 90 days | CC6.2 |
50
+ | IAM-05 | No `AdministratorAccess` attached directly to users | CC6.3 |
51
+ | S3-01 | Account-level public access block | CC6.6 |
52
+ | S3-02 | Default encryption on every bucket | CC6.1 |
53
+ | S3-03 | Per-bucket public access blocks | CC6.6 |
54
+ | NET-01 | Default security groups deny all traffic | CC6.6 |
55
+ | NET-02 | No SSH/RDP open to 0.0.0.0/0 | CC6.6 |
56
+ | NET-03 | VPC flow logs enabled | CC7.3 |
57
+ | NET-04 | Peering routes scoped tighter than /24 | CC6.6 |
58
+ | KMS-01 | Rotation on customer-managed keys | CC6.1 |
59
+ | LOG-01 | Active multi-region CloudTrail | CC7.2 |
60
+ | LOG-02 | EBS encryption-by-default | CC6.1 |
61
+
62
+ ## The full baseline checklist
63
+
64
+ The scanner covers what's machine-checkable. The complete ~30-control checklist — including the process controls (access reviews, IR severity levels, vuln SLAs, change management) — is in [CHECKLIST.md](CHECKLIST.md). It's the engineering half of compliance, free under CC BY 4.0.
65
+
66
+ ## The kit
67
+
68
+ Every failing check above has a ready-to-apply fix — a Terraform module or hardened template, plus the five corporate policies (Access Control, Change Management, Data Classification, Incident Response, Vulnerability Management) that enterprise reviewers ask for, pre-mapped to the code that enforces them.
69
+
70
+ > **⚡ Compliance Kit — $349 one-time.** Versioned Terraform modules + policies + control-to-code mapping, delivered as a private git repo. [Get it here.](https://YOUR-CHECKOUT-LINK)
71
+ >
72
+ > **🚀 Compliance Sprint — done with you in 14 days.** We run the scan, deploy the fixes into your account, customize the policies, and hand you a filled-out security questionnaire answer bank. [Book a free scan review.](https://calendly.com/banhjosh/30min)
73
+
74
+ ## FAQ
75
+
76
+ **Why not just use Prowler / ScoutSuite?** They're excellent and far more exhaustive — hundreds of findings across every service. This is deliberately the opposite: the 15 checks an enterprise security reviewer will actually probe first, with a fix attached to each. Run this to get deal-ready this week; run Prowler when you have a security team.
77
+
78
+ **Will this get me SOC 2 certified?** No tool does. This gets your technical controls to the baseline auditors expect, which is most of the engineering work and the part that blocks deals.
79
+
80
+ **License:** MIT (scanner), CC BY 4.0 (checklist).
@@ -0,0 +1,20 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ tests/test_checks.py
5
+ vigilai_scan/__init__.py
6
+ vigilai_scan/cli.py
7
+ vigilai_scan/registry.py
8
+ vigilai_scan/report.py
9
+ vigilai_scan.egg-info/PKG-INFO
10
+ vigilai_scan.egg-info/SOURCES.txt
11
+ vigilai_scan.egg-info/dependency_links.txt
12
+ vigilai_scan.egg-info/entry_points.txt
13
+ vigilai_scan.egg-info/requires.txt
14
+ vigilai_scan.egg-info/top_level.txt
15
+ vigilai_scan/checks/__init__.py
16
+ vigilai_scan/checks/iam.py
17
+ vigilai_scan/checks/kms.py
18
+ vigilai_scan/checks/logging_monitoring.py
19
+ vigilai_scan/checks/network.py
20
+ vigilai_scan/checks/s3.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vigilai-scan = vigilai_scan.cli:main
@@ -0,0 +1,4 @@
1
+ boto3>=1.28
2
+
3
+ [dev]
4
+ pytest>=7
@@ -0,0 +1 @@
1
+ vigilai_scan