cyntrisec 0.1.7__py3-none-any.whl
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.
- cyntrisec/__init__.py +3 -0
- cyntrisec/__main__.py +6 -0
- cyntrisec/aws/__init__.py +6 -0
- cyntrisec/aws/collectors/__init__.py +17 -0
- cyntrisec/aws/collectors/ec2.py +30 -0
- cyntrisec/aws/collectors/iam.py +116 -0
- cyntrisec/aws/collectors/lambda_.py +45 -0
- cyntrisec/aws/collectors/network.py +70 -0
- cyntrisec/aws/collectors/rds.py +38 -0
- cyntrisec/aws/collectors/s3.py +68 -0
- cyntrisec/aws/collectors/usage.py +188 -0
- cyntrisec/aws/credentials.py +153 -0
- cyntrisec/aws/normalizers/__init__.py +17 -0
- cyntrisec/aws/normalizers/ec2.py +115 -0
- cyntrisec/aws/normalizers/iam.py +182 -0
- cyntrisec/aws/normalizers/lambda_.py +83 -0
- cyntrisec/aws/normalizers/network.py +225 -0
- cyntrisec/aws/normalizers/rds.py +130 -0
- cyntrisec/aws/normalizers/s3.py +184 -0
- cyntrisec/aws/relationship_builder.py +1359 -0
- cyntrisec/aws/scanner.py +303 -0
- cyntrisec/cli/__init__.py +5 -0
- cyntrisec/cli/analyze.py +747 -0
- cyntrisec/cli/ask.py +412 -0
- cyntrisec/cli/can.py +307 -0
- cyntrisec/cli/comply.py +226 -0
- cyntrisec/cli/cuts.py +231 -0
- cyntrisec/cli/diff.py +332 -0
- cyntrisec/cli/errors.py +105 -0
- cyntrisec/cli/explain.py +348 -0
- cyntrisec/cli/main.py +114 -0
- cyntrisec/cli/manifest.py +893 -0
- cyntrisec/cli/output.py +117 -0
- cyntrisec/cli/remediate.py +643 -0
- cyntrisec/cli/report.py +462 -0
- cyntrisec/cli/scan.py +207 -0
- cyntrisec/cli/schemas.py +391 -0
- cyntrisec/cli/serve.py +164 -0
- cyntrisec/cli/setup.py +260 -0
- cyntrisec/cli/validate.py +101 -0
- cyntrisec/cli/waste.py +323 -0
- cyntrisec/core/__init__.py +31 -0
- cyntrisec/core/business_config.py +110 -0
- cyntrisec/core/business_logic.py +131 -0
- cyntrisec/core/compliance.py +437 -0
- cyntrisec/core/cost_estimator.py +301 -0
- cyntrisec/core/cuts.py +360 -0
- cyntrisec/core/diff.py +361 -0
- cyntrisec/core/graph.py +202 -0
- cyntrisec/core/paths.py +830 -0
- cyntrisec/core/schema.py +317 -0
- cyntrisec/core/simulator.py +371 -0
- cyntrisec/core/waste.py +309 -0
- cyntrisec/mcp/__init__.py +5 -0
- cyntrisec/mcp/server.py +862 -0
- cyntrisec/storage/__init__.py +7 -0
- cyntrisec/storage/filesystem.py +344 -0
- cyntrisec/storage/memory.py +113 -0
- cyntrisec/storage/protocol.py +92 -0
- cyntrisec-0.1.7.dist-info/METADATA +672 -0
- cyntrisec-0.1.7.dist-info/RECORD +65 -0
- cyntrisec-0.1.7.dist-info/WHEEL +4 -0
- cyntrisec-0.1.7.dist-info/entry_points.txt +2 -0
- cyntrisec-0.1.7.dist-info/licenses/LICENSE +190 -0
- cyntrisec-0.1.7.dist-info/licenses/NOTICE +5 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core module - Pydantic models, graph, and path algorithms.
|
|
3
|
+
|
|
4
|
+
No I/O dependencies. Pure data structures and algorithms.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from cyntrisec.core.graph import AwsGraph, GraphBuilder
|
|
8
|
+
from cyntrisec.core.paths import PathFinder, PathFinderConfig
|
|
9
|
+
from cyntrisec.core.schema import (
|
|
10
|
+
Asset,
|
|
11
|
+
AttackPath,
|
|
12
|
+
Finding,
|
|
13
|
+
FindingSeverity,
|
|
14
|
+
Relationship,
|
|
15
|
+
Snapshot,
|
|
16
|
+
SnapshotStatus,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Asset",
|
|
21
|
+
"AttackPath",
|
|
22
|
+
"AwsGraph",
|
|
23
|
+
"Finding",
|
|
24
|
+
"FindingSeverity",
|
|
25
|
+
"GraphBuilder",
|
|
26
|
+
"PathFinder",
|
|
27
|
+
"PathFinderConfig",
|
|
28
|
+
"Relationship",
|
|
29
|
+
"Snapshot",
|
|
30
|
+
"SnapshotStatus",
|
|
31
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Business Configuration - Define legitimate business context.
|
|
3
|
+
|
|
4
|
+
This module provides the schema for users to define what paths and assets
|
|
5
|
+
are "Business Critical" or "Legitimate Exposure". This inputs into the
|
|
6
|
+
graph engine to calculate the "Delta" (Attack Graph - Business Graph).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseConfig(BaseModel):
|
|
17
|
+
"""Base configuration for business rules."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(
|
|
20
|
+
extra="forbid",
|
|
21
|
+
str_strip_whitespace=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CriticalFlow(BaseConfig):
|
|
26
|
+
"""A required business flow between two assets."""
|
|
27
|
+
|
|
28
|
+
source: str = Field(..., description="Source asset ID or tag selector")
|
|
29
|
+
target: str = Field(..., description="Target asset ID or tag selector")
|
|
30
|
+
description: str | None = None
|
|
31
|
+
ports: list[int] | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EntrypointCriteria(BaseConfig):
|
|
35
|
+
"""Criteria for identifying legitimate entrypoints."""
|
|
36
|
+
|
|
37
|
+
by_id: list[str] = Field(default_factory=list)
|
|
38
|
+
by_tags: dict[str, str] = Field(default_factory=dict)
|
|
39
|
+
by_type: list[str] = Field(
|
|
40
|
+
default_factory=list,
|
|
41
|
+
description="Asset types considered safe entrypoints (e.g. 'cloudfront:distribution')",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BusinessConfig(BaseConfig):
|
|
46
|
+
"""
|
|
47
|
+
Configuration for defining legitimate business context.
|
|
48
|
+
|
|
49
|
+
This allows the engine to distinguish between:
|
|
50
|
+
- Business Critical paths (must exist)
|
|
51
|
+
- Legitimate Exposure (accepted risk)
|
|
52
|
+
- Attack Paths (unnecessary exposure)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
# Versioning for the config file format
|
|
56
|
+
version: Literal["1.0"] = "1.0"
|
|
57
|
+
|
|
58
|
+
# 1. Entrypoints: Where legitimate traffic enters
|
|
59
|
+
# Assets matching these criteria are considered "Authorized Exposure"
|
|
60
|
+
entrypoints: EntrypointCriteria = Field(default_factory=EntrypointCriteria)
|
|
61
|
+
|
|
62
|
+
# 2. Critical Flows: Traffic that MUST flow
|
|
63
|
+
# Explicitly authorized paths
|
|
64
|
+
critical_flows: list[CriticalFlow] = Field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
# 3. Global Allowlist: Tags that mark assets as "Business Critical"
|
|
67
|
+
# Assets with these tags are considered "Authorized" even if exposed.
|
|
68
|
+
# e.g. {"Environment": "Production", "App": "Frontend"}
|
|
69
|
+
global_allowlist: dict[str, str] = Field(default_factory=dict)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def load(cls, path: str) -> BusinessConfig:
|
|
73
|
+
"""Load configuration from a JSON or YAML file."""
|
|
74
|
+
import json
|
|
75
|
+
from pathlib import Path
|
|
76
|
+
|
|
77
|
+
path_obj = Path(path)
|
|
78
|
+
text = path_obj.read_text(encoding="utf-8")
|
|
79
|
+
|
|
80
|
+
data: object | None = None
|
|
81
|
+
suffix = path_obj.suffix.lower()
|
|
82
|
+
|
|
83
|
+
def parse_yaml() -> object | None:
|
|
84
|
+
import yaml
|
|
85
|
+
|
|
86
|
+
return yaml.safe_load(text)
|
|
87
|
+
|
|
88
|
+
def parse_json() -> object | None:
|
|
89
|
+
return json.loads(text)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
if suffix in {".yaml", ".yml"}:
|
|
93
|
+
data = parse_yaml()
|
|
94
|
+
elif suffix == ".json":
|
|
95
|
+
data = parse_json()
|
|
96
|
+
else:
|
|
97
|
+
# Default: YAML first (JSON is valid YAML), then JSON for clearer errors.
|
|
98
|
+
try:
|
|
99
|
+
data = parse_yaml()
|
|
100
|
+
except Exception:
|
|
101
|
+
data = parse_json()
|
|
102
|
+
except Exception as e:
|
|
103
|
+
raise ValueError(f"Failed to parse business config: {path}") from e
|
|
104
|
+
|
|
105
|
+
if data is None:
|
|
106
|
+
data = {}
|
|
107
|
+
if not isinstance(data, dict):
|
|
108
|
+
raise ValueError(f"Business config must be a mapping/object: {path}")
|
|
109
|
+
|
|
110
|
+
return cls(**data)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Business Logic Engine - Apply business context to the capability graph.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import fnmatch
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from cyntrisec.core.business_config import BusinessConfig
|
|
11
|
+
from cyntrisec.core.graph import AwsGraph
|
|
12
|
+
from cyntrisec.core.schema import Asset, AttackPath
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BusinessLogicEngine:
|
|
18
|
+
"""
|
|
19
|
+
Applies business rules to the graph to distinguish between:
|
|
20
|
+
- Business Critical (Must exist)
|
|
21
|
+
- Legitimate Exposure (Accepted risk)
|
|
22
|
+
- Unnecessary Exposure (Delta)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
LABEL_BUSINESS = "business_required"
|
|
26
|
+
LABEL_ENTRYPOINT = "business_entrypoint"
|
|
27
|
+
LABEL_AUTHORIZED = "authorized"
|
|
28
|
+
|
|
29
|
+
def __init__(self, graph: AwsGraph, config: BusinessConfig | None):
|
|
30
|
+
self.graph = graph
|
|
31
|
+
self.config = config
|
|
32
|
+
|
|
33
|
+
def apply_labels(self) -> None:
|
|
34
|
+
"""Apply business labels to assets and relationships."""
|
|
35
|
+
if not self.config:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
log.info("Applying business labels to graph...")
|
|
39
|
+
count = 0
|
|
40
|
+
|
|
41
|
+
# 1. Label Entrypoints
|
|
42
|
+
for asset in self.graph.all_assets():
|
|
43
|
+
if self._is_entrypoint(asset):
|
|
44
|
+
asset.labels.add(self.LABEL_BUSINESS)
|
|
45
|
+
asset.labels.add(self.LABEL_ENTRYPOINT)
|
|
46
|
+
count += 1
|
|
47
|
+
|
|
48
|
+
# 2. Global Allowlist
|
|
49
|
+
if self._matches_allowlist(asset):
|
|
50
|
+
asset.labels.add(self.LABEL_BUSINESS)
|
|
51
|
+
asset.labels.add(self.LABEL_AUTHORIZED)
|
|
52
|
+
count += 1
|
|
53
|
+
|
|
54
|
+
# 3. Critical Flows
|
|
55
|
+
# TODO: Requires PathFinder to trace paths between source/target
|
|
56
|
+
# Will be implemented in Pathfinding Upgrades phase
|
|
57
|
+
|
|
58
|
+
log.info("Labeled %d assets as business-critical", count)
|
|
59
|
+
|
|
60
|
+
def compute_delta(self, attack_paths: list[AttackPath]) -> list[AttackPath]:
|
|
61
|
+
"""
|
|
62
|
+
Compute the 'Delta' (Unnecessary Exposure).
|
|
63
|
+
|
|
64
|
+
Returns only the AttackPaths that are NOT fully legitimate.
|
|
65
|
+
A path is legitimate if EVERY step is labeled 'business_required' or 'authorized'.
|
|
66
|
+
"""
|
|
67
|
+
if not self.config:
|
|
68
|
+
# If no config, everything is potential exposure (or return all)
|
|
69
|
+
return attack_paths
|
|
70
|
+
|
|
71
|
+
delta_paths = []
|
|
72
|
+
for path in attack_paths:
|
|
73
|
+
if not self._is_path_legitimate(path):
|
|
74
|
+
delta_paths.append(path)
|
|
75
|
+
|
|
76
|
+
return delta_paths
|
|
77
|
+
|
|
78
|
+
def _is_entrypoint(self, asset: Asset) -> bool:
|
|
79
|
+
"""Check if asset matches entrypoint criteria."""
|
|
80
|
+
criteria = self.config.entrypoints
|
|
81
|
+
|
|
82
|
+
# By ID
|
|
83
|
+
if asset.aws_resource_id in criteria.by_id or asset.id in criteria.by_id:
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
# By Type
|
|
87
|
+
if asset.asset_type in criteria.by_type:
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
# By Tags
|
|
91
|
+
for tag_key, tag_pattern in criteria.by_tags.items():
|
|
92
|
+
val = asset.tags.get(tag_key)
|
|
93
|
+
if val and fnmatch.fnmatch(val, tag_pattern):
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
def _matches_allowlist(self, asset: Asset) -> bool:
|
|
99
|
+
"""Check if asset matches global allowlist tags."""
|
|
100
|
+
for tag_key, tag_pattern in self.config.global_allowlist.items():
|
|
101
|
+
val = asset.tags.get(tag_key)
|
|
102
|
+
if val and fnmatch.fnmatch(val, tag_pattern):
|
|
103
|
+
return True
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def _is_path_legitimate(self, path: AttackPath) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Check if an attack path is fully justified by business rules.
|
|
109
|
+
|
|
110
|
+
Strict mode: All assets and relationships must be labeled.
|
|
111
|
+
Relaxed mode: Just check if source and target are authorized?
|
|
112
|
+
For now, we implement a check: access must be authorized.
|
|
113
|
+
"""
|
|
114
|
+
# Optimized: Check if the *target* is authorized (e.g. "It's okay to access this DB")
|
|
115
|
+
# OR if the *source* is an authorized entrypoint AND the flow is business_required.
|
|
116
|
+
|
|
117
|
+
# For Phase 1, we'll check if the path consists of marked assets.
|
|
118
|
+
# Note: We need to check Edges too eventually.
|
|
119
|
+
|
|
120
|
+
for asset_id in path.path_asset_ids:
|
|
121
|
+
asset = self.graph.asset(asset_id)
|
|
122
|
+
if not asset: continue
|
|
123
|
+
|
|
124
|
+
# If any node in the chain is NOT business-required, the path is suspect.
|
|
125
|
+
# Exception: Maybe we allow traversal through unmarked nodes if the flow itself is marked?
|
|
126
|
+
# That requires Critical Flow labeling (Edge labeling).
|
|
127
|
+
|
|
128
|
+
if self.LABEL_BUSINESS not in asset.labels:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
return True
|