sdlckit 0.2.0__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.
- engine/__init__.py +157 -0
- engine/cli.py +246 -0
- engine/config/__init__.py +12 -0
- engine/config/_secret_detector.py +66 -0
- engine/config/loader.py +728 -0
- engine/config/plugins.py +224 -0
- engine/errors.py +82 -0
- engine/init/__init__.py +23 -0
- engine/init/config_gen.py +88 -0
- engine/init/harness.py +102 -0
- engine/init/integration.py +89 -0
- engine/init/manifest.py +128 -0
- engine/init/scaffolder.py +720 -0
- engine/initiative/__init__.py +17 -0
- engine/initiative/archive.py +130 -0
- engine/initiative/assignment.py +108 -0
- engine/initiative/knowledge.py +187 -0
- engine/initiative/manifest.py +274 -0
- engine/initiative/observation.py +154 -0
- engine/initiative/stage_review.py +132 -0
- engine/lifecycle/__init__.py +6 -0
- engine/lifecycle/context_resolver.py +431 -0
- engine/lifecycle/feedback.py +267 -0
- engine/lifecycle/manager.py +847 -0
- engine/lifecycle/reconciliation.py +308 -0
- engine/models/__init__.py +172 -0
- engine/models/artifacts.py +93 -0
- engine/models/config.py +175 -0
- engine/models/feedback.py +85 -0
- engine/models/initiative.py +103 -0
- engine/models/manifest.py +43 -0
- engine/models/plugin.py +45 -0
- engine/models/results.py +45 -0
- engine/models/skill.py +75 -0
- engine/models/state.py +132 -0
- engine/normalized_hash.py +21 -0
- engine/sage/__init__.py +11 -0
- engine/sage/gate_check.py +233 -0
- engine/sage/loop_check.py +88 -0
- engine/sage/skill_loader.py +276 -0
- sdlckit-0.2.0.dist-info/METADATA +226 -0
- sdlckit-0.2.0.dist-info/RECORD +46 -0
- sdlckit-0.2.0.dist-info/WHEEL +5 -0
- sdlckit-0.2.0.dist-info/entry_points.txt +2 -0
- sdlckit-0.2.0.dist-info/licenses/LICENSE +190 -0
- sdlckit-0.2.0.dist-info/top_level.txt +1 -0
engine/__init__.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""SDLCKit Engine — config, lifecycle, SAGE loop, initiative orchestration."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
|
|
5
|
+
from engine.config.loader import (
|
|
6
|
+
detect_inline_secrets,
|
|
7
|
+
load_lifecycle_config,
|
|
8
|
+
resolve_env_vars,
|
|
9
|
+
)
|
|
10
|
+
from engine.lifecycle.context_resolver import ContextResolver, build_dependency_graph
|
|
11
|
+
from engine.errors import (
|
|
12
|
+
ConfigError,
|
|
13
|
+
ConfigWarning,
|
|
14
|
+
ContextError,
|
|
15
|
+
CyclicDependencyError,
|
|
16
|
+
PluginError,
|
|
17
|
+
StateTransitionError,
|
|
18
|
+
)
|
|
19
|
+
from engine.lifecycle.feedback import FeedbackManager
|
|
20
|
+
from engine.sage.gate_check import extract_sections, run_gate_pass1
|
|
21
|
+
from engine.sage.loop_check import evaluate_loop_checks
|
|
22
|
+
from engine.normalized_hash import normalized_hash
|
|
23
|
+
from engine.models import (
|
|
24
|
+
Acknowledgment,
|
|
25
|
+
DimensionScore,
|
|
26
|
+
BudgetConfig,
|
|
27
|
+
ConnectorConfig,
|
|
28
|
+
DefaultsConfig,
|
|
29
|
+
DependencyGraph,
|
|
30
|
+
DimensionConfig,
|
|
31
|
+
EngineConfig,
|
|
32
|
+
FeedbackSignal,
|
|
33
|
+
GateCheckConfig,
|
|
34
|
+
GateCheckResult,
|
|
35
|
+
SAGEConfig,
|
|
36
|
+
InputDef,
|
|
37
|
+
LoopCheckConfig,
|
|
38
|
+
LoopCheckResult,
|
|
39
|
+
OverridesConfig,
|
|
40
|
+
PhaseConfig,
|
|
41
|
+
PhaseProvision,
|
|
42
|
+
PhaseRecord,
|
|
43
|
+
PhaseState,
|
|
44
|
+
LifecycleConfig,
|
|
45
|
+
LifecycleState,
|
|
46
|
+
PluginManifest,
|
|
47
|
+
PluginRef,
|
|
48
|
+
ReconciliationBudget,
|
|
49
|
+
ReconciliationPhaseEntry,
|
|
50
|
+
ReconciliationPlan,
|
|
51
|
+
ReconciliationSession,
|
|
52
|
+
RetryPolicy,
|
|
53
|
+
ScorerOutput,
|
|
54
|
+
SkillManifest,
|
|
55
|
+
StageConfig,
|
|
56
|
+
StalenessRecord,
|
|
57
|
+
SkillValidationResult,
|
|
58
|
+
TokenUsage,
|
|
59
|
+
)
|
|
60
|
+
from engine.sage.skill_loader import (
|
|
61
|
+
load_all_skills,
|
|
62
|
+
load_skill,
|
|
63
|
+
scan_required_sections,
|
|
64
|
+
validate_scorer_output,
|
|
65
|
+
validate_skill,
|
|
66
|
+
)
|
|
67
|
+
from engine.config.plugins import (
|
|
68
|
+
get_phase_plugin,
|
|
69
|
+
load_all_plugins,
|
|
70
|
+
load_plugin_manifest,
|
|
71
|
+
)
|
|
72
|
+
from engine.lifecycle.reconciliation import ReconciliationManager
|
|
73
|
+
from engine.lifecycle.manager import StateManager
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
# Version
|
|
77
|
+
"__version__",
|
|
78
|
+
# Config Loader
|
|
79
|
+
"load_lifecycle_config",
|
|
80
|
+
"resolve_env_vars",
|
|
81
|
+
"detect_inline_secrets",
|
|
82
|
+
# Plugin Loader
|
|
83
|
+
"load_plugin_manifest",
|
|
84
|
+
"load_all_plugins",
|
|
85
|
+
"get_phase_plugin",
|
|
86
|
+
# State Manager
|
|
87
|
+
"StateManager",
|
|
88
|
+
# Context Resolver
|
|
89
|
+
"ContextResolver",
|
|
90
|
+
"build_dependency_graph",
|
|
91
|
+
# SAGE — Gate Check
|
|
92
|
+
"run_gate_pass1",
|
|
93
|
+
"extract_sections",
|
|
94
|
+
# SAGE — Loop Check
|
|
95
|
+
"evaluate_loop_checks",
|
|
96
|
+
# Feedback & Reconciliation
|
|
97
|
+
"FeedbackManager",
|
|
98
|
+
"ReconciliationManager",
|
|
99
|
+
# Normalized Hash
|
|
100
|
+
"normalized_hash",
|
|
101
|
+
# Errors
|
|
102
|
+
"ConfigError",
|
|
103
|
+
"ConfigWarning",
|
|
104
|
+
"PluginError",
|
|
105
|
+
"StateTransitionError",
|
|
106
|
+
"CyclicDependencyError",
|
|
107
|
+
"ContextError",
|
|
108
|
+
# Models — Config
|
|
109
|
+
"LifecycleConfig",
|
|
110
|
+
"PhaseConfig",
|
|
111
|
+
"SAGEConfig",
|
|
112
|
+
"DimensionConfig",
|
|
113
|
+
"LoopCheckConfig",
|
|
114
|
+
"GateCheckConfig",
|
|
115
|
+
"PluginRef",
|
|
116
|
+
"ConnectorConfig",
|
|
117
|
+
"DefaultsConfig",
|
|
118
|
+
"BudgetConfig",
|
|
119
|
+
"OverridesConfig",
|
|
120
|
+
"EngineConfig",
|
|
121
|
+
"RetryPolicy",
|
|
122
|
+
# Models — Plugin
|
|
123
|
+
"PluginManifest",
|
|
124
|
+
"PhaseProvision",
|
|
125
|
+
"InputDef",
|
|
126
|
+
# Models — State
|
|
127
|
+
"LifecycleState",
|
|
128
|
+
"PhaseRecord",
|
|
129
|
+
"PhaseState",
|
|
130
|
+
"TokenUsage",
|
|
131
|
+
"StalenessRecord",
|
|
132
|
+
"Acknowledgment",
|
|
133
|
+
# Models — Feedback & Reconciliation
|
|
134
|
+
"FeedbackSignal",
|
|
135
|
+
"ReconciliationPlan",
|
|
136
|
+
"ReconciliationPhaseEntry",
|
|
137
|
+
"ReconciliationSession",
|
|
138
|
+
"ReconciliationBudget",
|
|
139
|
+
# Models — Stage
|
|
140
|
+
"StageConfig",
|
|
141
|
+
# Models — Graph
|
|
142
|
+
"DependencyGraph",
|
|
143
|
+
# Models — Results
|
|
144
|
+
"GateCheckResult",
|
|
145
|
+
"LoopCheckResult",
|
|
146
|
+
# SAGE — Skill Loader
|
|
147
|
+
"load_skill",
|
|
148
|
+
"load_all_skills",
|
|
149
|
+
"validate_skill",
|
|
150
|
+
"validate_scorer_output",
|
|
151
|
+
"scan_required_sections",
|
|
152
|
+
# Models — Skill
|
|
153
|
+
"SkillManifest",
|
|
154
|
+
"DimensionScore",
|
|
155
|
+
"ScorerOutput",
|
|
156
|
+
"SkillValidationResult",
|
|
157
|
+
]
|
engine/cli.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""SDLCKit CLI — entry point for sdlckit commands.
|
|
2
|
+
|
|
3
|
+
Provides the `sdlckit` console script registered via pyproject.toml.
|
|
4
|
+
Currently supports a single subcommand:
|
|
5
|
+
|
|
6
|
+
sdlckit init [TARGET] [--force] [--minimal]
|
|
7
|
+
|
|
8
|
+
The CLI parses arguments with argparse, delegates to the appropriate
|
|
9
|
+
engine module, and translates return values into shell exit codes:
|
|
10
|
+
0 — success (all files processed)
|
|
11
|
+
1 — partial success (some files skipped due to user modifications)
|
|
12
|
+
2 — fatal error (target not found, unrecoverable failure)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
import engine
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
prog="sdlckit",
|
|
26
|
+
description="SDLCKit — AI-governed SDLC lifecycle engine",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--version", "-V",
|
|
30
|
+
action="version",
|
|
31
|
+
version=f"sdlckit {engine.__version__}",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
35
|
+
|
|
36
|
+
# init subcommand
|
|
37
|
+
init_parser = subparsers.add_parser(
|
|
38
|
+
"init",
|
|
39
|
+
help="Initialize a repository for SDLCKit",
|
|
40
|
+
description="Scaffold a repository with SDLCKit runtime files, lifecycle config, and integration.",
|
|
41
|
+
)
|
|
42
|
+
init_parser.add_argument(
|
|
43
|
+
"target",
|
|
44
|
+
nargs="?",
|
|
45
|
+
default=".",
|
|
46
|
+
help="Target directory to initialize (default: current directory)",
|
|
47
|
+
)
|
|
48
|
+
init_parser.add_argument(
|
|
49
|
+
"--force", "-f",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Overwrite all files including user-modified; creates .bak backups",
|
|
52
|
+
)
|
|
53
|
+
init_parser.add_argument(
|
|
54
|
+
"--minimal", "-m",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Install only schemas, lifecycle config, and integration file",
|
|
57
|
+
)
|
|
58
|
+
init_parser.add_argument(
|
|
59
|
+
"--type", "-t",
|
|
60
|
+
dest="init_type",
|
|
61
|
+
choices=["component", "initiative"],
|
|
62
|
+
default="component",
|
|
63
|
+
help="Project type: component (default) or initiative",
|
|
64
|
+
)
|
|
65
|
+
init_parser.add_argument(
|
|
66
|
+
"--harness",
|
|
67
|
+
choices=["claude", "codex", "copilot", "cursor", "windsurf", "generic"],
|
|
68
|
+
default=None,
|
|
69
|
+
help="Target AI harness (auto-detected if omitted)",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# observe subcommand
|
|
73
|
+
observe_parser = subparsers.add_parser(
|
|
74
|
+
"observe",
|
|
75
|
+
help="Create a new observation",
|
|
76
|
+
description="Record an operational observation for cross-phase feedback.",
|
|
77
|
+
)
|
|
78
|
+
observe_parser.add_argument("title", help="Observation title")
|
|
79
|
+
observe_parser.add_argument("--description", "-d", default="", help="Observation body text")
|
|
80
|
+
observe_parser.add_argument(
|
|
81
|
+
"--source", "-s",
|
|
82
|
+
choices=["incident", "monitoring", "customer", "post-mortem", "manual"],
|
|
83
|
+
default="manual",
|
|
84
|
+
help="Observation source (default: manual)",
|
|
85
|
+
)
|
|
86
|
+
observe_parser.add_argument(
|
|
87
|
+
"--severity",
|
|
88
|
+
choices=["low", "medium", "high"],
|
|
89
|
+
default="medium",
|
|
90
|
+
help="Severity level (default: medium)",
|
|
91
|
+
)
|
|
92
|
+
observe_parser.add_argument(
|
|
93
|
+
"--components", "-c",
|
|
94
|
+
nargs="+",
|
|
95
|
+
required=True,
|
|
96
|
+
help="Affected component names",
|
|
97
|
+
)
|
|
98
|
+
observe_parser.add_argument("--tags", nargs="*", default=[], help="Optional tags")
|
|
99
|
+
observe_parser.add_argument(
|
|
100
|
+
"--state-dir",
|
|
101
|
+
default=".sdlckit",
|
|
102
|
+
help="State directory (default: .sdlckit)",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# assign subcommand
|
|
106
|
+
assign_parser = subparsers.add_parser(
|
|
107
|
+
"assign",
|
|
108
|
+
help="Import an assignment into a component repo",
|
|
109
|
+
description="Import an assignment file, creating a work directory and branch.",
|
|
110
|
+
)
|
|
111
|
+
assign_parser.add_argument("path", help="Path to the assignment markdown file")
|
|
112
|
+
assign_parser.add_argument(
|
|
113
|
+
"--work-dir",
|
|
114
|
+
default=".sdlckit/work",
|
|
115
|
+
help="Work directory (default: .sdlckit/work)",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# archive subcommand
|
|
119
|
+
archive_parser = subparsers.add_parser(
|
|
120
|
+
"archive",
|
|
121
|
+
help="Pre-merge cleanup for initiative or component repos",
|
|
122
|
+
description="Archive an initiative or component work item, updating manifests and cleaning up runtime state.",
|
|
123
|
+
)
|
|
124
|
+
archive_parser.add_argument("name", help="Initiative name or work item name to archive")
|
|
125
|
+
archive_parser.add_argument(
|
|
126
|
+
"--mode",
|
|
127
|
+
choices=["initiative", "component"],
|
|
128
|
+
default=None,
|
|
129
|
+
help="Archive mode (auto-detected if not specified)",
|
|
130
|
+
)
|
|
131
|
+
archive_parser.add_argument(
|
|
132
|
+
"--project-root",
|
|
133
|
+
default=".",
|
|
134
|
+
help="Project root directory (default: current directory)",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return parser
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main(argv: list[str] | None = None) -> int:
|
|
141
|
+
"""CLI entry point. Returns exit code."""
|
|
142
|
+
parser = _build_parser()
|
|
143
|
+
args = parser.parse_args(argv)
|
|
144
|
+
|
|
145
|
+
if args.command is None:
|
|
146
|
+
parser.print_help()
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
if args.command == "init":
|
|
150
|
+
if args.init_type == "initiative":
|
|
151
|
+
from engine.init.scaffolder import run_init_initiative
|
|
152
|
+
return run_init_initiative(target_dir=args.target)
|
|
153
|
+
from engine.init.harness import detect_harness
|
|
154
|
+
from engine.init.scaffolder import run_init
|
|
155
|
+
harness = args.harness
|
|
156
|
+
if harness is None:
|
|
157
|
+
from pathlib import Path
|
|
158
|
+
harness = detect_harness(Path(args.target).resolve())
|
|
159
|
+
if harness is None:
|
|
160
|
+
print("Could not auto-detect AI harness. Use --harness to specify one of: "
|
|
161
|
+
"claude, codex, copilot, cursor, windsurf, generic", file=sys.stderr)
|
|
162
|
+
return 2
|
|
163
|
+
return run_init(
|
|
164
|
+
target_dir=args.target,
|
|
165
|
+
force=args.force,
|
|
166
|
+
minimal=args.minimal,
|
|
167
|
+
harness=harness,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if args.command == "observe":
|
|
171
|
+
from pathlib import Path
|
|
172
|
+
from engine.initiative.observation import create_observation
|
|
173
|
+
obs_dir = Path(args.state_dir) / "observations"
|
|
174
|
+
try:
|
|
175
|
+
path = create_observation(
|
|
176
|
+
obs_dir,
|
|
177
|
+
title=args.title,
|
|
178
|
+
description=args.description,
|
|
179
|
+
source=args.source,
|
|
180
|
+
severity=args.severity,
|
|
181
|
+
components=args.components,
|
|
182
|
+
tags=args.tags,
|
|
183
|
+
)
|
|
184
|
+
print(f"Observation created: {path}")
|
|
185
|
+
return 0
|
|
186
|
+
except Exception as e:
|
|
187
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
188
|
+
return 2
|
|
189
|
+
|
|
190
|
+
if args.command == "assign":
|
|
191
|
+
from pathlib import Path
|
|
192
|
+
from engine.initiative.assignment import import_assignment
|
|
193
|
+
try:
|
|
194
|
+
dest = import_assignment(Path(args.path), Path(args.work_dir))
|
|
195
|
+
print(f"Assignment imported: {dest}")
|
|
196
|
+
return 0
|
|
197
|
+
except Exception as e:
|
|
198
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
199
|
+
return 2
|
|
200
|
+
|
|
201
|
+
if args.command == "archive":
|
|
202
|
+
from pathlib import Path
|
|
203
|
+
from engine.initiative.archive import archive_component, archive_initiative
|
|
204
|
+
root = Path(args.project_root)
|
|
205
|
+
mode = args.mode
|
|
206
|
+
if mode is None:
|
|
207
|
+
if (root / "sdlc-initiative.yaml").exists():
|
|
208
|
+
mode = "initiative"
|
|
209
|
+
elif (root / "sdlc.yaml").exists():
|
|
210
|
+
mode = "component"
|
|
211
|
+
else:
|
|
212
|
+
print("Error: cannot detect mode — no sdlc.yaml or sdlc-initiative.yaml found", file=sys.stderr)
|
|
213
|
+
return 2
|
|
214
|
+
try:
|
|
215
|
+
if mode == "initiative":
|
|
216
|
+
result = archive_initiative(args.name, root)
|
|
217
|
+
else:
|
|
218
|
+
result = archive_component(args.name, root)
|
|
219
|
+
if result.warnings:
|
|
220
|
+
for w in result.warnings:
|
|
221
|
+
print(f"Warning: {w}", file=sys.stderr)
|
|
222
|
+
if result.manifest_updated:
|
|
223
|
+
print(f"Manifest updated: {args.name} → archived")
|
|
224
|
+
if result.profile_diffs:
|
|
225
|
+
print(f"Profile diffs to review: {len(result.profile_diffs)}")
|
|
226
|
+
for d in result.profile_diffs:
|
|
227
|
+
print(f" {d}")
|
|
228
|
+
if result.knowledge_diffs:
|
|
229
|
+
print(f"Knowledge diffs to review: {len(result.knowledge_diffs)}")
|
|
230
|
+
for d in result.knowledge_diffs:
|
|
231
|
+
print(f" {d}")
|
|
232
|
+
if result.dirs_removed:
|
|
233
|
+
print(f"Directories removed: {len(result.dirs_removed)}")
|
|
234
|
+
for d in result.dirs_removed:
|
|
235
|
+
print(f" {d}")
|
|
236
|
+
return 0
|
|
237
|
+
except Exception as e:
|
|
238
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
239
|
+
return 2
|
|
240
|
+
|
|
241
|
+
parser.print_help()
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
sys.exit(main())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Configuration loading — lifecycle config, plugins, secret detection."""
|
|
2
|
+
|
|
3
|
+
from engine.config.loader import (
|
|
4
|
+
detect_inline_secrets,
|
|
5
|
+
load_lifecycle_config,
|
|
6
|
+
resolve_env_vars,
|
|
7
|
+
)
|
|
8
|
+
from engine.config.plugins import (
|
|
9
|
+
get_phase_plugin,
|
|
10
|
+
load_all_plugins,
|
|
11
|
+
load_plugin_manifest,
|
|
12
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Inline secret pattern detection (internal module)."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
KNOWN_PREFIXES = (
|
|
7
|
+
"xoxb-",
|
|
8
|
+
"xoxp-",
|
|
9
|
+
"xoxa-",
|
|
10
|
+
"ghp_",
|
|
11
|
+
"gho_",
|
|
12
|
+
"ghs_",
|
|
13
|
+
"sk-",
|
|
14
|
+
"AKIA",
|
|
15
|
+
"rk_live_",
|
|
16
|
+
"rk_test_",
|
|
17
|
+
"pk_live_",
|
|
18
|
+
"pk_test_",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_BASE64_PATTERN = re.compile(r"^[A-Za-z0-9+/=]{32,}$")
|
|
22
|
+
_ALNUM_PATTERN = re.compile(r"^[A-Za-z0-9]{40,}$")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _shannon_entropy(s: str) -> float:
|
|
26
|
+
"""Calculate Shannon entropy in bits per character."""
|
|
27
|
+
if not s:
|
|
28
|
+
return 0.0
|
|
29
|
+
freq: dict[str, int] = {}
|
|
30
|
+
for c in s:
|
|
31
|
+
freq[c] = freq.get(c, 0) + 1
|
|
32
|
+
length = len(s)
|
|
33
|
+
return -sum(
|
|
34
|
+
(count / length) * math.log2(count / length) for count in freq.values()
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def detect_inline_secret(value: str) -> bool:
|
|
39
|
+
"""Check if a string value matches known secret patterns.
|
|
40
|
+
|
|
41
|
+
Returns True if value matches:
|
|
42
|
+
- Known token prefixes (Slack, GitHub, OpenAI, AWS, Stripe)
|
|
43
|
+
- High-entropy alphanumeric strings (>40 chars, entropy > 4.0 bits/char)
|
|
44
|
+
- Base64 blobs (>32 chars matching base64 character set)
|
|
45
|
+
"""
|
|
46
|
+
if not isinstance(value, str):
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
# Skip env var references — they're the recommended approach
|
|
50
|
+
if value.startswith("${") and value.endswith("}"):
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
# Check known prefixes
|
|
54
|
+
for prefix in KNOWN_PREFIXES:
|
|
55
|
+
if value.startswith(prefix):
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
# Check high-entropy alphanumeric strings
|
|
59
|
+
if _ALNUM_PATTERN.match(value) and _shannon_entropy(value) > 4.0:
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
# Check base64 blobs
|
|
63
|
+
if _BASE64_PATTERN.match(value):
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
return False
|