anvil 0.30.1__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.
- anvil/__init__.py +0 -0
- anvil/_components.py +282 -0
- anvil/actions.py +17 -0
- anvil/benchmark.py +50 -0
- anvil/cli.py +1487 -0
- anvil/descriptors.py +155 -0
- anvil/execution_context.py +24 -0
- anvil/processor_loader.py +470 -0
- anvil/processor_validation.py +90 -0
- anvil/processors/__init__.py +0 -0
- anvil/processors/html_report.py +645 -0
- anvil/processors/sarif_report.py +348 -0
- anvil/provider_loader.py +170 -0
- anvil/providers/__init__.py +1 -0
- anvil/providers/aws/__init__.py +17 -0
- anvil/providers/aws/account.py +178 -0
- anvil/providers/aws/account_resolver.py +66 -0
- anvil/providers/aws/auth.py +148 -0
- anvil/providers/aws/config.py +13 -0
- anvil/providers/aws/organization.py +215 -0
- anvil/providers/aws/provider.py +497 -0
- anvil/providers/aws/regions.py +65 -0
- anvil/providers/aws/session.py +258 -0
- anvil/providers/aws/tasks/__init__.py +1 -0
- anvil/providers/aws/tasks/compare_asg_to_cluster_instances.py +167 -0
- anvil/providers/aws/tasks/count_subnets_with_timings.py +140 -0
- anvil/providers/aws/tasks/count_vpc.py +68 -0
- anvil/providers/aws/tasks/detect_deprecated_lambda_runtimes.py +204 -0
- anvil/providers/aws/tasks/get_aws_inline_policies.py +309 -0
- anvil/providers/aws/tasks/get_organization_structure.py +197 -0
- anvil/providers/aws/tasks/list_lambdas_by_runtime.py +145 -0
- anvil/providers/aws/tasks/remove_iam_user.py +243 -0
- anvil/providers/aws/tasks/remove_missing_group_assignments.py +311 -0
- anvil/providers/azure/__init__.py +20 -0
- anvil/providers/azure/provider.py +830 -0
- anvil/providers/azure/tasks/__init__.py +1 -0
- anvil/providers/azure/tasks/count_resource_groups.py +128 -0
- anvil/providers/base.py +278 -0
- anvil/providers/gcp/__init__.py +17 -0
- anvil/providers/gcp/provider.py +585 -0
- anvil/providers/gcp/tasks/__init__.py +1 -0
- anvil/providers/gcp/tasks/get_project_info.py +137 -0
- anvil/providers/github/__init__.py +20 -0
- anvil/providers/github/provider.py +1611 -0
- anvil/providers/github/tasks/__init__.py +1 -0
- anvil/providers/github/tasks/_rest.py +246 -0
- anvil/providers/github/tasks/audit_branch_protection.py +82 -0
- anvil/providers/github/tasks/audit_repo_security_settings.py +76 -0
- anvil/providers/github/tasks/audit_rulesets.py +73 -0
- anvil/providers/github/tasks/list_code_scanning_alerts.py +75 -0
- anvil/providers/github/tasks/list_dependabot_alerts.py +78 -0
- anvil/providers/github/tasks/list_secret_scanning_alerts.py +78 -0
- anvil/providers/github/tasks/search_code.py +283 -0
- anvil/providers/tasks/__init__.py +1 -0
- anvil/providers/tasks/noop.py +58 -0
- anvil/providers/tasks/noop_fail.py +62 -0
- anvil/regions.py +151 -0
- anvil/result_query.py +537 -0
- anvil/results.py +325 -0
- anvil/runner.py +1257 -0
- anvil/schemas/common.schema.v2.json +174 -0
- anvil/schemas/targets.schema.v2.json +102 -0
- anvil/task_context.py +35 -0
- anvil/task_loader.py +520 -0
- anvil/task_validation.py +109 -0
- anvil/validators.py +194 -0
- anvil-0.30.1.dist-info/METADATA +471 -0
- anvil-0.30.1.dist-info/RECORD +71 -0
- anvil-0.30.1.dist-info/WHEEL +4 -0
- anvil-0.30.1.dist-info/entry_points.txt +3 -0
- anvil-0.30.1.dist-info/licenses/LICENSE +21 -0
anvil/__init__.py
ADDED
|
File without changes
|
anvil/_components.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Shared lazy component discovery and resolution primitives.
|
|
2
|
+
|
|
3
|
+
This module deliberately knows nothing about task, processor, or provider runtime
|
|
4
|
+
contracts. Component-specific loaders validate those contracts after selection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
import pkgutil
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
from importlib.metadata import EntryPoint
|
|
16
|
+
from inspect import Parameter, signature
|
|
17
|
+
from types import MappingProxyType
|
|
18
|
+
from typing import Generic, TypeVar
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ComponentKind(StrEnum):
|
|
25
|
+
"""Kinds of extension components understood by Anvil."""
|
|
26
|
+
|
|
27
|
+
TASK = "task"
|
|
28
|
+
PROCESSOR = "processor"
|
|
29
|
+
PROVIDER = "provider"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ComponentOrigin(StrEnum):
|
|
33
|
+
"""Origin of a discovered component."""
|
|
34
|
+
|
|
35
|
+
STOCK = "stock"
|
|
36
|
+
PLUGIN = "plugin"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class ComponentSource:
|
|
41
|
+
"""Structured origin information for a discovered component."""
|
|
42
|
+
|
|
43
|
+
origin: ComponentOrigin
|
|
44
|
+
package: str
|
|
45
|
+
label: str
|
|
46
|
+
distribution: str | None = None
|
|
47
|
+
entry_point_group: str | None = None
|
|
48
|
+
entry_point_name: str | None = None
|
|
49
|
+
provider: str | None = None
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
return self.label
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class ComponentDescriptor(Generic[T]):
|
|
57
|
+
"""A named component and a loader that imports only that component."""
|
|
58
|
+
|
|
59
|
+
name: str
|
|
60
|
+
source: ComponentSource
|
|
61
|
+
load: Callable[[], T] = field(compare=False, hash=False, repr=False)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class DiscoveryIssue:
|
|
66
|
+
"""A non-fatal problem encountered while inspecting a component source."""
|
|
67
|
+
|
|
68
|
+
name: str
|
|
69
|
+
source: ComponentSource
|
|
70
|
+
error: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True, slots=True)
|
|
74
|
+
class ComponentCatalog(Generic[T]):
|
|
75
|
+
"""Immutable discovery snapshot retaining every candidate by name."""
|
|
76
|
+
|
|
77
|
+
descriptors: tuple[ComponentDescriptor[T], ...]
|
|
78
|
+
issues: tuple[DiscoveryIssue, ...] = ()
|
|
79
|
+
inventory: Mapping[str, tuple[ComponentDescriptor[T], ...]] = field(init=False)
|
|
80
|
+
|
|
81
|
+
def __post_init__(self) -> None:
|
|
82
|
+
grouped: dict[str, list[ComponentDescriptor[T]]] = defaultdict(list)
|
|
83
|
+
for descriptor in self.descriptors:
|
|
84
|
+
grouped[descriptor.name].append(descriptor)
|
|
85
|
+
inventory = {
|
|
86
|
+
name: tuple(candidates) for name, candidates in sorted(grouped.items())
|
|
87
|
+
}
|
|
88
|
+
object.__setattr__(self, "inventory", MappingProxyType(inventory))
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def build(
|
|
92
|
+
cls,
|
|
93
|
+
descriptors: Iterable[ComponentDescriptor[T]],
|
|
94
|
+
issues: Iterable[DiscoveryIssue] = (),
|
|
95
|
+
) -> ComponentCatalog[T]:
|
|
96
|
+
"""Build a deterministically ordered immutable catalog."""
|
|
97
|
+
|
|
98
|
+
return cls(
|
|
99
|
+
descriptors=tuple(sorted(descriptors, key=_descriptor_sort_key)),
|
|
100
|
+
issues=tuple(sorted(issues, key=_issue_sort_key)),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ComponentResolutionError(RuntimeError):
|
|
105
|
+
"""Raised when a component name is missing or ambiguous."""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class PackageComponentSource(Generic[T]):
|
|
110
|
+
"""Discover immediate public children of one importable package root."""
|
|
111
|
+
|
|
112
|
+
package_name: str
|
|
113
|
+
source: ComponentSource
|
|
114
|
+
component_loader: Callable[[str, str, ComponentSource], T]
|
|
115
|
+
reserved_children: frozenset[str] = frozenset()
|
|
116
|
+
|
|
117
|
+
def discover(
|
|
118
|
+
self, *, issue_name: str | None = None
|
|
119
|
+
) -> tuple[tuple[ComponentDescriptor[T], ...], tuple[DiscoveryIssue, ...]]:
|
|
120
|
+
"""Return lazy child descriptors without importing child modules."""
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
package = importlib.import_module(self.package_name)
|
|
124
|
+
except Exception as error:
|
|
125
|
+
return (), (
|
|
126
|
+
DiscoveryIssue(
|
|
127
|
+
name=issue_name or self.package_name,
|
|
128
|
+
source=self.source,
|
|
129
|
+
error=f"package import failed ({error})",
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
package_path = getattr(package, "__path__", None)
|
|
134
|
+
if package_path is None:
|
|
135
|
+
return (), (
|
|
136
|
+
DiscoveryIssue(
|
|
137
|
+
name=issue_name or self.package_name,
|
|
138
|
+
source=self.source,
|
|
139
|
+
error="component source must reference a package",
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
descriptors: list[ComponentDescriptor[T]] = []
|
|
144
|
+
for module_info in pkgutil.iter_modules(package_path):
|
|
145
|
+
name = module_info.name
|
|
146
|
+
if name.startswith("_") or name in self.reserved_children:
|
|
147
|
+
continue
|
|
148
|
+
descriptors.append(
|
|
149
|
+
ComponentDescriptor(
|
|
150
|
+
name=name,
|
|
151
|
+
source=self.source,
|
|
152
|
+
load=lambda n=name: self.component_loader(
|
|
153
|
+
self.package_name, n, self.source
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
return tuple(descriptors), ()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ComponentResolver(Generic[T]):
|
|
161
|
+
"""Resolve unique names and load only selected components."""
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
*,
|
|
166
|
+
kind: ComponentKind,
|
|
167
|
+
catalog: ComponentCatalog[T],
|
|
168
|
+
error_type: type[Exception] = ComponentResolutionError,
|
|
169
|
+
context: str | None = None,
|
|
170
|
+
) -> None:
|
|
171
|
+
self._kind = kind
|
|
172
|
+
self._catalog = catalog
|
|
173
|
+
self._error_type = error_type
|
|
174
|
+
self._context = context
|
|
175
|
+
|
|
176
|
+
def catalog(self) -> ComponentCatalog[T]:
|
|
177
|
+
"""Return the resolver's immutable discovery snapshot."""
|
|
178
|
+
|
|
179
|
+
return self._catalog
|
|
180
|
+
|
|
181
|
+
def descriptor(self, name: str) -> ComponentDescriptor[T]:
|
|
182
|
+
"""Return the sole descriptor for name or raise an actionable error."""
|
|
183
|
+
|
|
184
|
+
candidates = self._catalog.inventory.get(name, ())
|
|
185
|
+
context = f" {self._context}" if self._context else ""
|
|
186
|
+
if not candidates:
|
|
187
|
+
available = ", ".join(self._catalog.inventory) or "none"
|
|
188
|
+
raise self._error_type(
|
|
189
|
+
f"Unknown {self._kind.value}{context} '{name}'. "
|
|
190
|
+
f"Available {self._kind.value}s: {available}"
|
|
191
|
+
)
|
|
192
|
+
if len(candidates) > 1:
|
|
193
|
+
sources = ", ".join(str(candidate.source) for candidate in candidates)
|
|
194
|
+
raise self._error_type(
|
|
195
|
+
f"{self._kind.value.capitalize()} '{name}' is ambiguous{context}; "
|
|
196
|
+
f"found in multiple sources: {sources}"
|
|
197
|
+
)
|
|
198
|
+
return candidates[0]
|
|
199
|
+
|
|
200
|
+
def load(self, name: str) -> T:
|
|
201
|
+
"""Load the uniquely selected component."""
|
|
202
|
+
|
|
203
|
+
return self.descriptor(name).load()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def validate_keyword_only_invocation(
|
|
207
|
+
callable_object: Callable[..., object], *, keyword_names: frozenset[str]
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Validate that a callable accepts the supplied runtime keywords.
|
|
210
|
+
|
|
211
|
+
Additional optional keyword-only parameters and ``**kwargs`` are allowed.
|
|
212
|
+
Additional required parameters are rejected because the runtime cannot
|
|
213
|
+
supply them.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
callable_object: Callable to inspect.
|
|
217
|
+
keyword_names: Runtime keyword names that will always be supplied.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
ValueError: If the signature cannot accept the runtime invocation.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
callable_signature = signature(callable_object)
|
|
225
|
+
except (TypeError, ValueError) as error:
|
|
226
|
+
raise ValueError("unable to inspect callable signature") from error
|
|
227
|
+
|
|
228
|
+
parameters = callable_signature.parameters
|
|
229
|
+
missing = keyword_names - set(parameters)
|
|
230
|
+
if missing:
|
|
231
|
+
raise ValueError(f"missing required parameters: {sorted(missing)}")
|
|
232
|
+
|
|
233
|
+
unsupported_parameters = sorted(
|
|
234
|
+
parameter.name
|
|
235
|
+
for parameter in parameters.values()
|
|
236
|
+
if parameter.kind not in {Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD}
|
|
237
|
+
)
|
|
238
|
+
if unsupported_parameters:
|
|
239
|
+
raise ValueError(f"parameters must be keyword-only: {unsupported_parameters}")
|
|
240
|
+
|
|
241
|
+
invocation_kwargs = {name: object() for name in keyword_names}
|
|
242
|
+
try:
|
|
243
|
+
callable_signature.bind(**invocation_kwargs)
|
|
244
|
+
except TypeError as error:
|
|
245
|
+
raise ValueError(f"cannot be invoked with runtime keywords: {error}") from error
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def source_from_entry_point(
|
|
249
|
+
*,
|
|
250
|
+
entry_point: EntryPoint,
|
|
251
|
+
package: str,
|
|
252
|
+
label_prefix: str = "plugin:",
|
|
253
|
+
provider: str | None = None,
|
|
254
|
+
) -> ComponentSource:
|
|
255
|
+
"""Build structured source metadata for a package entry point."""
|
|
256
|
+
|
|
257
|
+
distribution = entry_point.dist.name if entry_point.dist is not None else None
|
|
258
|
+
distribution_label = distribution or "unpackaged"
|
|
259
|
+
return ComponentSource(
|
|
260
|
+
origin=ComponentOrigin.PLUGIN,
|
|
261
|
+
package=package,
|
|
262
|
+
label=f"{label_prefix} {distribution_label}",
|
|
263
|
+
distribution=distribution,
|
|
264
|
+
entry_point_group=entry_point.group,
|
|
265
|
+
entry_point_name=entry_point.name,
|
|
266
|
+
provider=provider,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _descriptor_sort_key(
|
|
271
|
+
descriptor: ComponentDescriptor[T],
|
|
272
|
+
) -> tuple[str, str, str, str]:
|
|
273
|
+
return (
|
|
274
|
+
descriptor.source.label,
|
|
275
|
+
descriptor.name,
|
|
276
|
+
descriptor.source.entry_point_name or "",
|
|
277
|
+
descriptor.source.package,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _issue_sort_key(issue: DiscoveryIssue) -> tuple[str, str, str]:
|
|
282
|
+
return (issue.source.label, issue.name, issue.error)
|
anvil/actions.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(slots=True)
|
|
7
|
+
class ActionRecorder:
|
|
8
|
+
"""
|
|
9
|
+
Collects actions performed during task execution.
|
|
10
|
+
|
|
11
|
+
Created by the engine and passed into tasks.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
actions: list[str]
|
|
15
|
+
|
|
16
|
+
def record(self, message: str) -> None:
|
|
17
|
+
self.actions.append(message)
|
anvil/benchmark.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Iterator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BenchmarkRecorder:
|
|
9
|
+
"""
|
|
10
|
+
Small opt-in recorder for benchmark phase timings and metadata.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self, *, enabled: bool = False, data: dict[str, object] | None = None
|
|
15
|
+
) -> None:
|
|
16
|
+
self._data = data if data is not None else ({} if enabled else None)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def enabled(self) -> bool:
|
|
20
|
+
return self._data is not None
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def data(self) -> dict[str, object] | None:
|
|
24
|
+
return self._data
|
|
25
|
+
|
|
26
|
+
@contextmanager
|
|
27
|
+
def phase(self, name: str) -> Iterator[None]:
|
|
28
|
+
if self._data is None:
|
|
29
|
+
yield
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
started = time.perf_counter()
|
|
33
|
+
try:
|
|
34
|
+
yield
|
|
35
|
+
finally:
|
|
36
|
+
self._data[name] = time.perf_counter() - started
|
|
37
|
+
|
|
38
|
+
def set(self, name: str, value: object) -> None:
|
|
39
|
+
if self._data is not None:
|
|
40
|
+
self._data[name] = value
|
|
41
|
+
|
|
42
|
+
def pop(self, name: str) -> object | None:
|
|
43
|
+
if self._data is None:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
return self._data.pop(name, None)
|
|
47
|
+
|
|
48
|
+
def update(self, values: dict[str, object]) -> None:
|
|
49
|
+
if self._data is not None:
|
|
50
|
+
self._data.update(values)
|