Skip to content

Commit a110896

Browse files
authored
Merge pull request #65 from wrhalpin/copilot/fix-ci-workflow-failures-another-one
Fix CI failures: ruff lint errors, pylint slots=True on Python 3.9, and integration test skipping
2 parents b92e435 + 6e145b9 commit a110896

19 files changed

Lines changed: 481 additions & 165 deletions
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
from .leak_scanner import LeakScanner, LeakFinding
2-
from .duplicate_detector import DuplicateDetector
3-
from .unsafe_patterns import UnsafePatternDetector
1+
from .duplicate_detector import DuplicateDetector as DuplicateDetector
2+
from .leak_scanner import LeakFinding as LeakFinding
3+
from .leak_scanner import LeakScanner as LeakScanner
4+
from .unsafe_patterns import UnsafePatternDetector as UnsafePatternDetector
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from collections import defaultdict
2-
from typing import Dict, Iterable, List
2+
from collections.abc import Iterable
3+
34

45
class DuplicateDetector:
5-
def find_duplicates(self, values: Iterable[str]) -> Dict[str, List[str]]:
6+
def find_duplicates(self, values: Iterable[str]) -> dict[str, list[str]]:
67
index = defaultdict(list)
7-
for value in values: index[value].append(value)
8+
for value in values:
9+
index[value].append(value)
810
return {k: v for k, v in index.items() if len(v) > 1}
Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,60 @@
11
from __future__ import annotations
2+
3+
import re
4+
from collections.abc import Iterable
25
from dataclasses import dataclass
36
from pathlib import Path
4-
from typing import Iterable, List
5-
import re
67

7-
@dataclass(slots=True)
8+
9+
@dataclass
810
class LeakFinding:
911
path: str
1012
line_number: int
1113
severity: str
1214
rule: str
1315
snippet: str
1416

17+
1518
class LeakScanner:
1619
DEFAULT_PATTERNS = {
1720
"aws_access_key": re.compile(r"AKIA[0-9A-Z]{16}"),
1821
"private_key_header": re.compile(r"-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----"),
19-
"generic_token_assignment": re.compile(r"(api[_-]?key|token|secret)\s*[:=]\s*['\"][^'\"]{8,}['\"]", re.IGNORECASE),
22+
"generic_token_assignment": re.compile(
23+
r"(api[_-]?key|token|secret)\s*[:=]\s*['\"][^'\"]{8,}['\"]", re.IGNORECASE
24+
),
2025
}
26+
2127
def __init__(self, allowlist: Iterable[str] | None = None) -> None:
2228
self.allowlist = set(allowlist or [])
23-
def scan_paths(self, paths: Iterable[str]) -> List[LeakFinding]:
24-
findings: List[LeakFinding] = []
29+
30+
def scan_paths(self, paths: Iterable[str]) -> list[LeakFinding]:
31+
findings: list[LeakFinding] = []
2532
for root in paths:
2633
for path in Path(root).rglob("*"):
27-
if path.is_file() and not any(part in {".git", ".venv", "__pycache__"} for part in path.parts):
34+
if path.is_file() and not any(
35+
part in {".git", ".venv", "__pycache__"} for part in path.parts
36+
):
2837
findings.extend(self._scan_file(path))
2938
return findings
30-
def _scan_file(self, path: Path) -> List[LeakFinding]:
31-
try: text = path.read_text(encoding="utf-8", errors="ignore")
32-
except OSError: return []
33-
out: List[LeakFinding] = []
39+
40+
def _scan_file(self, path: Path) -> list[LeakFinding]:
41+
try:
42+
text = path.read_text(encoding="utf-8", errors="ignore")
43+
except OSError:
44+
return []
45+
out: list[LeakFinding] = []
3446
for i, line in enumerate(text.splitlines(), start=1):
35-
if line.strip() in self.allowlist: continue
47+
if line.strip() in self.allowlist:
48+
continue
3649
for rule, pattern in self.DEFAULT_PATTERNS.items():
3750
if pattern.search(line):
38-
out.append(LeakFinding(str(path), i, "high" if rule != "generic_token_assignment" else "medium", rule, line.strip()[:200]))
51+
out.append(
52+
LeakFinding(
53+
str(path),
54+
i,
55+
"high" if rule != "generic_token_assignment" else "medium",
56+
rule,
57+
line.strip()[:200],
58+
)
59+
)
3960
return out
Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,37 @@
11
from __future__ import annotations
2+
23
from dataclasses import dataclass
3-
from typing import Any, Dict, List
4+
from typing import Any
5+
46

5-
@dataclass(slots=True)
7+
@dataclass
68
class UnsafePatternFinding:
79
location: str
810
rule: str
911
message: str
1012

13+
1114
class UnsafePatternDetector:
12-
def inspect_connector_config(self, config: Dict[str, Any]) -> List[UnsafePatternFinding]:
13-
findings: List[UnsafePatternFinding] = []
15+
def inspect_connector_config(self, config: dict[str, Any]) -> list[UnsafePatternFinding]:
16+
findings: list[UnsafePatternFinding] = []
1417
credentials = config.get("credentials", {})
15-
if not isinstance(credentials, dict): return findings
18+
if not isinstance(credentials, dict):
19+
return findings
1620
for key, value in credentials.items():
1721
if isinstance(value, str):
18-
findings.append(UnsafePatternFinding(f"credentials.{key}", "plain_text_secret", "credential should use secret_ref instead of inline string"))
22+
findings.append(
23+
UnsafePatternFinding(
24+
f"credentials.{key}",
25+
"plain_text_secret",
26+
"credential should use secret_ref instead of inline string",
27+
)
28+
)
1929
elif isinstance(value, dict) and "value" in value:
20-
findings.append(UnsafePatternFinding(f"credentials.{key}", "embedded_secret_value", "credential dictionary embeds raw value instead of reference"))
30+
findings.append(
31+
UnsafePatternFinding(
32+
f"credentials.{key}",
33+
"embedded_secret_value",
34+
"credential dictionary embeds raw value instead of reference",
35+
)
36+
)
2137
return findings
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
1-
from .broker import SecretsBroker
2-
from .models import SecretRef, SecretValue, SecretLease, SecretMetadata, ProviderCapabilities, StoreSecretRequest, SecretVersionInfo, AuditEvent
1+
from .broker import SecretsBroker as SecretsBroker
2+
from .models import AuditEvent as AuditEvent
3+
from .models import ProviderCapabilities as ProviderCapabilities
4+
from .models import SecretLease as SecretLease
5+
from .models import SecretMetadata as SecretMetadata
6+
from .models import SecretRef as SecretRef
7+
from .models import SecretValue as SecretValue
8+
from .models import SecretVersionInfo as SecretVersionInfo
9+
from .models import StoreSecretRequest as StoreSecretRequest
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
from __future__ import annotations
2+
23
from datetime import datetime
3-
from typing import List
4+
45
from .models import AuditEvent, SecretRef
56

7+
68
class InMemoryAuditRecorder:
79
def __init__(self) -> None:
8-
self.events: List[AuditEvent] = []
9-
def record(self, *, action: str, actor: str, ref: SecretRef, allowed: bool, provider: str, reason: str = "") -> None:
10-
self.events.append(AuditEvent(action, actor, ref.to_uri(), allowed, provider, datetime.utcnow(), reason))
10+
self.events: list[AuditEvent] = []
11+
12+
def record(
13+
self,
14+
*,
15+
action: str,
16+
actor: str,
17+
ref: SecretRef,
18+
allowed: bool,
19+
provider: str,
20+
reason: str = "",
21+
) -> None:
22+
self.events.append(
23+
AuditEvent(action, actor, ref.to_uri(), allowed, provider, datetime.utcnow(), reason)
24+
)
Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,96 @@
11
from __future__ import annotations
2-
from typing import Dict
2+
33
from urllib.parse import parse_qs, urlparse
4+
45
from .audit import InMemoryAuditRecorder
56
from .exceptions import SecretPolicyError, SecretProviderError, UnsupportedProviderAction
67
from .models import SecretRef, SecretValue, SecretVersionInfo, StoreSecretRequest
78
from .policy import SecretPolicyEngine
89
from .providers.base import SecretProvider
910

11+
1012
class SecretsBroker:
11-
def __init__(self, providers: Dict[str, SecretProvider], policy: SecretPolicyEngine, audit: InMemoryAuditRecorder | None = None) -> None:
13+
def __init__(
14+
self,
15+
providers: dict[str, SecretProvider],
16+
policy: SecretPolicyEngine,
17+
audit: InMemoryAuditRecorder | None = None,
18+
) -> None:
1219
self.providers = providers
1320
self.policy = policy
1421
self.audit = audit or InMemoryAuditRecorder()
22+
1523
def parse_ref(self, uri: str) -> SecretRef:
16-
parsed = urlparse(uri); params = parse_qs(parsed.query)
17-
return SecretRef(provider=parsed.scheme, vault=parsed.netloc or None, path=parsed.path.lstrip("/"), version=params.get("version", [None])[0])
24+
parsed = urlparse(uri)
25+
params = parse_qs(parsed.query)
26+
return SecretRef(
27+
provider=parsed.scheme,
28+
vault=parsed.netloc or None,
29+
path=parsed.path.lstrip("/"),
30+
version=params.get("version", [None])[0],
31+
)
32+
1833
def resolve(self, ref: SecretRef, *, caller: str) -> SecretValue:
1934
decision = self.policy.decide(ref, action="resolve", caller=caller)
20-
self.audit.record(action="resolve", actor=caller, ref=ref, allowed=decision.allowed, provider=ref.provider, reason=decision.reason)
21-
if not decision.allowed: raise SecretPolicyError(decision.reason)
35+
self.audit.record(
36+
action="resolve",
37+
actor=caller,
38+
ref=ref,
39+
allowed=decision.allowed,
40+
provider=ref.provider,
41+
reason=decision.reason,
42+
)
43+
if not decision.allowed:
44+
raise SecretPolicyError(decision.reason)
2245
provider = self._provider(ref.provider)
23-
if not provider.capabilities().supports_read: raise UnsupportedProviderAction(f"provider does not support resolve: {ref.provider}")
46+
if not provider.capabilities().supports_read:
47+
raise UnsupportedProviderAction(f"provider does not support resolve: {ref.provider}")
2448
return provider.resolve(ref)
49+
2550
def store(self, request: StoreSecretRequest, *, caller: str) -> SecretVersionInfo:
26-
decision = self.policy.decide(request.ref, action="store", caller=caller, overwrite=request.allow_overwrite)
27-
self.audit.record(action="store", actor=caller, ref=request.ref, allowed=decision.allowed, provider=request.ref.provider, reason=decision.reason)
28-
if not decision.allowed: raise SecretPolicyError(decision.reason)
29-
provider = self._provider(request.ref.provider); caps = provider.capabilities()
30-
if not caps.supports_write: raise UnsupportedProviderAction(f"provider does not support store: {request.ref.provider}")
31-
if request.tags and not caps.supports_tagging: raise UnsupportedProviderAction(f"provider does not support tags: {request.ref.provider}")
51+
decision = self.policy.decide(
52+
request.ref, action="store", caller=caller, overwrite=request.allow_overwrite
53+
)
54+
self.audit.record(
55+
action="store",
56+
actor=caller,
57+
ref=request.ref,
58+
allowed=decision.allowed,
59+
provider=request.ref.provider,
60+
reason=decision.reason,
61+
)
62+
if not decision.allowed:
63+
raise SecretPolicyError(decision.reason)
64+
provider = self._provider(request.ref.provider)
65+
caps = provider.capabilities()
66+
if not caps.supports_write:
67+
raise UnsupportedProviderAction(
68+
f"provider does not support store: {request.ref.provider}"
69+
)
70+
if request.tags and not caps.supports_tagging:
71+
raise UnsupportedProviderAction(
72+
f"provider does not support tags: {request.ref.provider}"
73+
)
3274
return provider.store(request)
75+
3376
def checkout(self, ref: SecretRef, *, caller: str):
3477
decision = self.policy.decide(ref, action="checkout", caller=caller)
35-
self.audit.record(action="checkout", actor=caller, ref=ref, allowed=decision.allowed, provider=ref.provider, reason=decision.reason)
36-
if not decision.allowed: raise SecretPolicyError(decision.reason)
78+
self.audit.record(
79+
action="checkout",
80+
actor=caller,
81+
ref=ref,
82+
allowed=decision.allowed,
83+
provider=ref.provider,
84+
reason=decision.reason,
85+
)
86+
if not decision.allowed:
87+
raise SecretPolicyError(decision.reason)
3788
provider = self._provider(ref.provider)
38-
if not provider.capabilities().supports_checkout: raise UnsupportedProviderAction(f"provider does not support checkout: {ref.provider}")
89+
if not provider.capabilities().supports_checkout:
90+
raise UnsupportedProviderAction(f"provider does not support checkout: {ref.provider}")
3991
return provider.checkout(ref)
92+
4093
def _provider(self, name: str) -> SecretProvider:
41-
if name not in self.providers: raise SecretProviderError(f"unknown provider: {name}")
94+
if name not in self.providers:
95+
raise SecretProviderError(f"unknown provider: {name}")
4296
return self.providers[name]
Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
class SecretError(Exception): pass
2-
class SecretPolicyError(SecretError): pass
3-
class SecretProviderError(SecretError): pass
4-
class UnsupportedProviderAction(SecretProviderError): pass
1+
class SecretError(Exception):
2+
pass
3+
4+
5+
class SecretPolicyError(SecretError):
6+
pass
7+
8+
9+
class SecretProviderError(SecretError):
10+
pass
11+
12+
13+
class UnsupportedProviderAction(SecretProviderError):
14+
pass

0 commit comments

Comments
 (0)