adaptersentry 1.0.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.
- adaptersentry/__init__.py +14 -0
- adaptersentry/__main__.py +4 -0
- adaptersentry/analyzer.py +554 -0
- adaptersentry/cli/__init__.py +1 -0
- adaptersentry/cli/batch.py +353 -0
- adaptersentry/cli/main.py +68 -0
- adaptersentry/cli/scan.py +302 -0
- adaptersentry/detectors/__init__.py +27 -0
- adaptersentry/detectors/cross_layer.py +112 -0
- adaptersentry/detectors/entropy.py +93 -0
- adaptersentry/detectors/init_detector.py +111 -0
- adaptersentry/detectors/outlier.py +211 -0
- adaptersentry/detectors/wasserstein.py +175 -0
- adaptersentry/engine/__init__.py +17 -0
- adaptersentry/engine/cache.py +270 -0
- adaptersentry/engine/config.py +137 -0
- adaptersentry/engine/feature_extractor.py +561 -0
- adaptersentry/engine/identity.py +144 -0
- adaptersentry/engine/manifest.py +293 -0
- adaptersentry/engine/orchestrator.py +318 -0
- adaptersentry/engine/orchestrator_ray.py +353 -0
- adaptersentry/engine/result_sink.py +221 -0
- adaptersentry/engine/schemas/__init__.py +27 -0
- adaptersentry/engine/schemas/cache.py +69 -0
- adaptersentry/engine/schemas/combined_report.py +93 -0
- adaptersentry/engine/schemas/identity.py +82 -0
- adaptersentry/engine/schemas/requests.py +85 -0
- adaptersentry/engine/schemas/scan_result.py +148 -0
- adaptersentry/engine/schemas/scoring.py +104 -0
- adaptersentry/engine/schemas/signals.py +90 -0
- adaptersentry/engine/worker.py +363 -0
- adaptersentry/features/__init__.py +6 -0
- adaptersentry/features/delta_norm.py +232 -0
- adaptersentry/features/distribution.py +234 -0
- adaptersentry/features/entropy_compression.py +174 -0
- adaptersentry/features/inter_layer_similarity.py +195 -0
- adaptersentry/features/layer_stats.py +74 -0
- adaptersentry/features/tensor_stats.py +139 -0
- adaptersentry/integrations/__init__.py +1 -0
- adaptersentry/integrations/github_action.py +97 -0
- adaptersentry/parsers/__init__.py +24 -0
- adaptersentry/parsers/metadata.py +172 -0
- adaptersentry/parsers/safetensors.py +375 -0
- adaptersentry/reporters/__init__.py +7 -0
- adaptersentry/reporters/json.py +51 -0
- adaptersentry/reporters/sarif.py +223 -0
- adaptersentry/reporters/text.py +171 -0
- adaptersentry/reporting/__init__.py +0 -0
- adaptersentry/reporting/human_summary.py +293 -0
- adaptersentry/reporting/per_layer.py +198 -0
- adaptersentry/schemas/__init__.py +33 -0
- adaptersentry/schemas/adapter_metadata.py +78 -0
- adaptersentry/schemas/adapter_report.py +188 -0
- adaptersentry/schemas/confidence_score.py +146 -0
- adaptersentry/schemas/distribution_features.py +110 -0
- adaptersentry/schemas/entropy_compression_features.py +117 -0
- adaptersentry/schemas/errors.py +154 -0
- adaptersentry/schemas/finding.py +252 -0
- adaptersentry/schemas/inter_layer_similarity_features.py +80 -0
- adaptersentry/schemas/norm_features.py +64 -0
- adaptersentry/schemas/per_layer_finding.py +73 -0
- adaptersentry/schemas/score_breakdown.py +108 -0
- adaptersentry/schemas/scoring_policy.py +99 -0
- adaptersentry/schemas/tensor_record.py +125 -0
- adaptersentry/scoring/__init__.py +6 -0
- adaptersentry/scoring/confidence.py +231 -0
- adaptersentry/scoring/ensemble.py +399 -0
- adaptersentry/scoring/risk_scorer.py +128 -0
- adaptersentry/scoring/score_breakdown.py +558 -0
- adaptersentry/version.py +1 -0
- adaptersentry-1.0.1.dist-info/METADATA +750 -0
- adaptersentry-1.0.1.dist-info/RECORD +83 -0
- adaptersentry-1.0.1.dist-info/WHEEL +5 -0
- adaptersentry-1.0.1.dist-info/entry_points.txt +4 -0
- adaptersentry-1.0.1.dist-info/licenses/LICENSE +202 -0
- adaptersentry-1.0.1.dist-info/top_level.txt +2 -0
- benchmarks/__init__.py +1 -0
- benchmarks/check_regression.py +161 -0
- benchmarks/corpus.py +135 -0
- benchmarks/harness.py +497 -0
- benchmarks/hub_scanner.py +936 -0
- benchmarks/report.py +389 -0
- benchmarks/run_real.py +107 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""batch subcommand — scan a corpus of LoRA adapters.
|
|
2
|
+
|
|
3
|
+
Scans multiple .safetensors files with a process-based worker pool,
|
|
4
|
+
incremental caching, and full resumability after crashes.
|
|
5
|
+
|
|
6
|
+
Exit codes:
|
|
7
|
+
0 — batch completed; all jobs in terminal state
|
|
8
|
+
1 — batch failed or was interrupted
|
|
9
|
+
2 — any job produced findings at or above --fail-on threshold
|
|
10
|
+
|
|
11
|
+
Output:
|
|
12
|
+
results/<run_id>/ — per-adapter JSON files (summary-json)
|
|
13
|
+
results/<run_id>/run.jsonl — append-only audit log (all results)
|
|
14
|
+
results/<run_id>/run_summary.json — batch stats and top findings
|
|
15
|
+
[results/<run_id>/*.debug.json] — debug-json per adapter (if --debug)
|
|
16
|
+
|
|
17
|
+
Cache:
|
|
18
|
+
~/.adaptersentry/cache/ — content-addressed result cache
|
|
19
|
+
~/.adaptersentry/manifest.sqlite — per-run state machine
|
|
20
|
+
|
|
21
|
+
Resume:
|
|
22
|
+
adaptersentry batch --input-dir ... --run-id <id> --resume
|
|
23
|
+
Resets non-terminal jobs and re-runs from where the batch stopped.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
30
|
+
import sys
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
_SEVERITIES = ("LOW", "MEDIUM", "HIGH", "CRITICAL")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_parser(subparsers: Any) -> None:
|
|
41
|
+
"""Register the ``batch`` subcommand."""
|
|
42
|
+
p = subparsers.add_parser(
|
|
43
|
+
"batch",
|
|
44
|
+
help="Scan a corpus of LoRA adapters (batch mode with caching and resume)",
|
|
45
|
+
description=__doc__,
|
|
46
|
+
formatter_class=__import__("argparse").RawDescriptionHelpFormatter,
|
|
47
|
+
)
|
|
48
|
+
source = p.add_mutually_exclusive_group(required=True)
|
|
49
|
+
source.add_argument(
|
|
50
|
+
"--input-dir",
|
|
51
|
+
type=Path,
|
|
52
|
+
metavar="DIR",
|
|
53
|
+
help="Directory to scan recursively for .safetensors files",
|
|
54
|
+
)
|
|
55
|
+
source.add_argument(
|
|
56
|
+
"--input-list",
|
|
57
|
+
type=Path,
|
|
58
|
+
metavar="FILE",
|
|
59
|
+
help="Text file with one adapter path per line",
|
|
60
|
+
)
|
|
61
|
+
p.add_argument(
|
|
62
|
+
"--run-id",
|
|
63
|
+
default=None,
|
|
64
|
+
metavar="ID",
|
|
65
|
+
help=(
|
|
66
|
+
"Batch run identifier. If omitted, auto-generated from timestamp. "
|
|
67
|
+
"Use the same ID with --resume to continue a previous run."
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
p.add_argument(
|
|
71
|
+
"--workers",
|
|
72
|
+
type=int,
|
|
73
|
+
default=4,
|
|
74
|
+
metavar="N",
|
|
75
|
+
help="Number of parallel worker processes (default: 4, minimum: 1)",
|
|
76
|
+
)
|
|
77
|
+
p.add_argument(
|
|
78
|
+
"--output-dir",
|
|
79
|
+
type=Path,
|
|
80
|
+
default=Path("results"),
|
|
81
|
+
metavar="DIR",
|
|
82
|
+
help="Output directory for per-adapter JSON files (default: ./results)",
|
|
83
|
+
)
|
|
84
|
+
p.add_argument(
|
|
85
|
+
"--cache-dir",
|
|
86
|
+
type=Path,
|
|
87
|
+
default=None,
|
|
88
|
+
metavar="DIR",
|
|
89
|
+
help=(
|
|
90
|
+
"Cache store root (default: ~/.adaptersentry/cache). "
|
|
91
|
+
"Set to /dev/null to disable caching."
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
p.add_argument(
|
|
95
|
+
"--no-cache",
|
|
96
|
+
action="store_true",
|
|
97
|
+
help="Disable result caching (always re-scan)",
|
|
98
|
+
)
|
|
99
|
+
p.add_argument(
|
|
100
|
+
"--resume",
|
|
101
|
+
action="store_true",
|
|
102
|
+
help="Resume a previous run: reset non-terminal jobs and continue",
|
|
103
|
+
)
|
|
104
|
+
p.add_argument(
|
|
105
|
+
"--force-rescan",
|
|
106
|
+
action="store_true",
|
|
107
|
+
help="Re-scan all adapters, ignoring existing terminal manifest state",
|
|
108
|
+
)
|
|
109
|
+
p.add_argument(
|
|
110
|
+
"--debug",
|
|
111
|
+
action="store_true",
|
|
112
|
+
help="Write .debug.json files with per-layer statistics alongside summary JSON",
|
|
113
|
+
)
|
|
114
|
+
p.add_argument(
|
|
115
|
+
"--fail-on",
|
|
116
|
+
choices=_SEVERITIES,
|
|
117
|
+
default=None,
|
|
118
|
+
metavar="SEVERITY",
|
|
119
|
+
help="Exit with code 2 if any finding meets or exceeds SEVERITY",
|
|
120
|
+
)
|
|
121
|
+
p.add_argument(
|
|
122
|
+
"--rank",
|
|
123
|
+
type=int,
|
|
124
|
+
default=None,
|
|
125
|
+
metavar="R",
|
|
126
|
+
help="Declared LoRA rank r (applied to all adapters in the batch)",
|
|
127
|
+
)
|
|
128
|
+
p.add_argument(
|
|
129
|
+
"--mode",
|
|
130
|
+
choices=("full", "fast"),
|
|
131
|
+
default="full",
|
|
132
|
+
dest="scan_mode",
|
|
133
|
+
help=(
|
|
134
|
+
"Scan depth: full (default) — all detectors at full depth; "
|
|
135
|
+
"fast — truncated SVD, sampling, no IsolationForest on large tensors. "
|
|
136
|
+
"Use fast for high-throughput corpus screening."
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
p.add_argument(
|
|
140
|
+
"--backend",
|
|
141
|
+
choices=("mp", "ray"),
|
|
142
|
+
default="mp",
|
|
143
|
+
help=(
|
|
144
|
+
"Worker backend: mp (default) — multiprocessing.Pool; "
|
|
145
|
+
"ray — Ray actor pool (requires pip install adaptersentry[ray]). "
|
|
146
|
+
"Ray adds crash isolation, BLAS thread fix, and horizontal scaling."
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
p.add_argument(
|
|
150
|
+
"--ray-address",
|
|
151
|
+
default=None,
|
|
152
|
+
metavar="ADDRESS",
|
|
153
|
+
help=(
|
|
154
|
+
"Ray cluster address (e.g. ray://head-node:10001). "
|
|
155
|
+
"Omit to start a local Ray cluster on this machine. "
|
|
156
|
+
"Only used with --backend ray."
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def run(args: Any) -> int:
|
|
162
|
+
"""Execute the batch subcommand."""
|
|
163
|
+
from adaptersentry.engine.config import AnalyzerConfig, ScanMode
|
|
164
|
+
from adaptersentry.engine.manifest import ManifestDB
|
|
165
|
+
from adaptersentry.engine.cache import CacheStore
|
|
166
|
+
from adaptersentry.engine.orchestrator import (
|
|
167
|
+
build_manifest, run_batch, resume_after_failure,
|
|
168
|
+
)
|
|
169
|
+
from adaptersentry.schemas.finding import Severity
|
|
170
|
+
|
|
171
|
+
backend = getattr(args, "backend", "mp")
|
|
172
|
+
|
|
173
|
+
if args.workers < 1:
|
|
174
|
+
print("error: --workers must be >= 1", file=sys.stderr)
|
|
175
|
+
return 1
|
|
176
|
+
|
|
177
|
+
if getattr(args, "ray_address", None) and backend != "ray":
|
|
178
|
+
logger.warning("--ray-address is ignored when --backend is not 'ray'")
|
|
179
|
+
|
|
180
|
+
# ── Run ID ────────────────────────────────────────────────────────────────
|
|
181
|
+
run_id = args.run_id or f"run_{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}"
|
|
182
|
+
logger.info("Batch run: %s", run_id)
|
|
183
|
+
|
|
184
|
+
# ── Resolve paths ─────────────────────────────────────────────────────────
|
|
185
|
+
input_paths = _collect_paths(args)
|
|
186
|
+
if not input_paths:
|
|
187
|
+
print("error: no .safetensors files found in the specified input", file=sys.stderr)
|
|
188
|
+
return 1
|
|
189
|
+
|
|
190
|
+
logger.info("Found %d adapter path(s)", len(input_paths))
|
|
191
|
+
|
|
192
|
+
# ── Directories ───────────────────────────────────────────────────────────
|
|
193
|
+
output_dir: Path = args.output_dir
|
|
194
|
+
run_dir = output_dir / run_id
|
|
195
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
run_jsonl = run_dir / "run.jsonl"
|
|
197
|
+
|
|
198
|
+
# ── Analyzer config hash ──────────────────────────────────────────────────
|
|
199
|
+
scan_mode = ScanMode(getattr(args, "scan_mode", "full"))
|
|
200
|
+
config = AnalyzerConfig(scan_mode=scan_mode)
|
|
201
|
+
config_hash = config.config_hash()
|
|
202
|
+
logger.info("Analyzer config hash: %s scan_mode=%s", config_hash, scan_mode.value)
|
|
203
|
+
|
|
204
|
+
# ── Manifest ──────────────────────────────────────────────────────────────
|
|
205
|
+
manifest_path = Path.home() / ".adaptersentry" / "manifest.sqlite"
|
|
206
|
+
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
|
|
208
|
+
with ManifestDB.open(manifest_path) as manifest_db:
|
|
209
|
+
# ── Resume ────────────────────────────────────────────────────────────
|
|
210
|
+
if args.resume:
|
|
211
|
+
n_reset, n_terminal = resume_after_failure(run_id, manifest_db)
|
|
212
|
+
print(f"Resume: {n_reset} job(s) reset, {n_terminal} already complete.")
|
|
213
|
+
|
|
214
|
+
# ── Build manifest ────────────────────────────────────────────────────
|
|
215
|
+
requests = build_manifest(
|
|
216
|
+
input_paths=input_paths,
|
|
217
|
+
run_id=run_id,
|
|
218
|
+
manifest_db=manifest_db,
|
|
219
|
+
force_rescan=args.force_rescan,
|
|
220
|
+
scan_mode=scan_mode.value,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if not requests:
|
|
224
|
+
stats = manifest_db.stats(run_id)
|
|
225
|
+
print(
|
|
226
|
+
f"All {sum(stats.values())} adapter(s) already in terminal state "
|
|
227
|
+
f"for run {run_id}. Use --force-rescan to re-scan."
|
|
228
|
+
)
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
print(f"Scanning {len(requests)} adapter(s) with {args.workers} worker(s)...")
|
|
232
|
+
|
|
233
|
+
# ── Cache ─────────────────────────────────────────────────────────────
|
|
234
|
+
cache_root: Path | None = None
|
|
235
|
+
cache_store: CacheStore | None = None
|
|
236
|
+
|
|
237
|
+
if not args.no_cache:
|
|
238
|
+
if args.cache_dir:
|
|
239
|
+
cache_root = args.cache_dir
|
|
240
|
+
else:
|
|
241
|
+
cache_root = Path.home() / ".adaptersentry" / "cache"
|
|
242
|
+
cache_root.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
cache_store = CacheStore.open(cache_root)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
# ── Run batch ─────────────────────────────────────────────────────
|
|
247
|
+
if backend == "ray":
|
|
248
|
+
from adaptersentry.engine.orchestrator_ray import run_batch_ray
|
|
249
|
+
stats = run_batch_ray(
|
|
250
|
+
requests=requests,
|
|
251
|
+
manifest_db=manifest_db,
|
|
252
|
+
cache_store=cache_store,
|
|
253
|
+
results_dir=run_dir,
|
|
254
|
+
run_jsonl_path=run_jsonl,
|
|
255
|
+
analyzer_config_hash=config_hash,
|
|
256
|
+
cache_root=cache_root,
|
|
257
|
+
n_workers=args.workers,
|
|
258
|
+
write_debug=args.debug,
|
|
259
|
+
ray_address=getattr(args, "ray_address", None),
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
stats = run_batch(
|
|
263
|
+
requests=requests,
|
|
264
|
+
manifest_db=manifest_db,
|
|
265
|
+
cache_store=cache_store,
|
|
266
|
+
results_dir=run_dir,
|
|
267
|
+
run_jsonl_path=run_jsonl,
|
|
268
|
+
analyzer_config_hash=config_hash,
|
|
269
|
+
cache_root=cache_root,
|
|
270
|
+
n_workers=args.workers,
|
|
271
|
+
write_debug=args.debug,
|
|
272
|
+
)
|
|
273
|
+
finally:
|
|
274
|
+
if cache_store is not None:
|
|
275
|
+
cache_store.close()
|
|
276
|
+
|
|
277
|
+
# ── Summary ───────────────────────────────────────────────────────────────
|
|
278
|
+
total = sum(stats.values())
|
|
279
|
+
print(
|
|
280
|
+
f"\nBatch complete: {total} adapter(s) scanned\n"
|
|
281
|
+
f" ok={stats.get('ok', 0)} degraded={stats.get('degraded', 0)} "
|
|
282
|
+
f"failed={stats.get('failed', 0)} cached={stats.get('cached', 0)}"
|
|
283
|
+
)
|
|
284
|
+
print(f"Results written to: {run_dir}")
|
|
285
|
+
print(f"Audit log: {run_jsonl}")
|
|
286
|
+
|
|
287
|
+
_write_run_summary(run_dir, run_id, stats)
|
|
288
|
+
|
|
289
|
+
# ── fail-on threshold ─────────────────────────────────────────────────────
|
|
290
|
+
if args.fail_on and run_jsonl.exists():
|
|
291
|
+
threshold = _SEVERITIES.index(args.fail_on)
|
|
292
|
+
triggered = _check_fail_on(run_jsonl, threshold)
|
|
293
|
+
if triggered:
|
|
294
|
+
print(f"\nFINDINGS at or above {args.fail_on} detected. Exiting with code 2.")
|
|
295
|
+
return 2
|
|
296
|
+
|
|
297
|
+
if stats.get("failed", 0) > 0:
|
|
298
|
+
return 1
|
|
299
|
+
|
|
300
|
+
return 0
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _collect_paths(args: Any) -> list[Path]:
|
|
304
|
+
"""Collect adapter paths from --input-dir or --input-list."""
|
|
305
|
+
paths: list[Path] = []
|
|
306
|
+
if args.input_dir:
|
|
307
|
+
d = Path(args.input_dir)
|
|
308
|
+
if not d.is_dir():
|
|
309
|
+
logger.error("--input-dir is not a directory: %s", d)
|
|
310
|
+
return []
|
|
311
|
+
paths = list(d.rglob("*.safetensors"))
|
|
312
|
+
elif args.input_list:
|
|
313
|
+
lf = Path(args.input_list)
|
|
314
|
+
if not lf.exists():
|
|
315
|
+
logger.error("--input-list file not found: %s", lf)
|
|
316
|
+
return []
|
|
317
|
+
lines = lf.read_text().splitlines()
|
|
318
|
+
paths = list(dict.fromkeys(Path(line.strip()) for line in lines if line.strip()))
|
|
319
|
+
return paths
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _write_run_summary(run_dir: Path, run_id: str, stats: dict) -> None:
|
|
323
|
+
from datetime import datetime, timezone
|
|
324
|
+
summary = {
|
|
325
|
+
"run_id": run_id,
|
|
326
|
+
"completed_at": datetime.now(timezone.utc).isoformat(),
|
|
327
|
+
"stats": stats,
|
|
328
|
+
}
|
|
329
|
+
(run_dir / "run_summary.json").write_text(
|
|
330
|
+
json.dumps(summary, indent=2), encoding="utf-8"
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
_SEV_ORDER = {s: i for i, s in enumerate(_SEVERITIES)}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _check_fail_on(run_jsonl: Path, threshold_idx: int) -> bool:
|
|
338
|
+
"""Return True if any result in the JSONL has a finding at or above threshold."""
|
|
339
|
+
try:
|
|
340
|
+
for line in run_jsonl.read_text().splitlines():
|
|
341
|
+
if not line.strip():
|
|
342
|
+
continue
|
|
343
|
+
try:
|
|
344
|
+
obj = json.loads(line)
|
|
345
|
+
for finding in obj.get("findings", []):
|
|
346
|
+
sev = finding.get("severity", "LOW")
|
|
347
|
+
if _SEV_ORDER.get(sev, 0) >= threshold_idx:
|
|
348
|
+
return True
|
|
349
|
+
except json.JSONDecodeError:
|
|
350
|
+
continue
|
|
351
|
+
except OSError:
|
|
352
|
+
pass
|
|
353
|
+
return False
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Top-level CLI entry point for AdapterSentry.
|
|
2
|
+
|
|
3
|
+
Commands
|
|
4
|
+
--------
|
|
5
|
+
adaptersentry scan ADAPTER [options] — run M1 static analysis
|
|
6
|
+
adaptersentry --version — print version and exit
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import logging
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _build_root_parser() -> argparse.ArgumentParser:
|
|
17
|
+
from adaptersentry.version import __version__
|
|
18
|
+
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
prog="adaptersentry",
|
|
21
|
+
description="AdapterSentry — static security scanner for LoRA adapters",
|
|
22
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
23
|
+
epilog=(
|
|
24
|
+
"Examples:\n"
|
|
25
|
+
" adaptersentry scan adapter.safetensors\n"
|
|
26
|
+
" adaptersentry scan adapter.safetensors --format json --output report.json\n"
|
|
27
|
+
" adaptersentry scan adapter.safetensors --format sarif --fail-on HIGH\n"
|
|
28
|
+
" adaptersentry --version\n"
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--version",
|
|
33
|
+
action="version",
|
|
34
|
+
version=f"adaptersentry {__version__}",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--verbose",
|
|
38
|
+
action="store_true",
|
|
39
|
+
help="Enable DEBUG logging",
|
|
40
|
+
)
|
|
41
|
+
return parser
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> None:
|
|
45
|
+
"""Primary entry point for ``adaptersentry`` and ``python -m adaptersentry``."""
|
|
46
|
+
from adaptersentry.cli.scan import build_parser as build_scan_parser, run as run_scan
|
|
47
|
+
from adaptersentry.cli.batch import build_parser as build_batch_parser, run as run_batch
|
|
48
|
+
|
|
49
|
+
root = _build_root_parser()
|
|
50
|
+
subparsers = root.add_subparsers(dest="command", metavar="COMMAND")
|
|
51
|
+
build_scan_parser(subparsers)
|
|
52
|
+
build_batch_parser(subparsers)
|
|
53
|
+
|
|
54
|
+
args = root.parse_args()
|
|
55
|
+
|
|
56
|
+
logging.basicConfig(
|
|
57
|
+
level=logging.DEBUG if args.verbose else logging.WARNING,
|
|
58
|
+
format="%(levelname)s %(name)s: %(message)s",
|
|
59
|
+
stream=sys.stderr,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if args.command == "scan":
|
|
63
|
+
sys.exit(run_scan(args))
|
|
64
|
+
elif args.command == "batch":
|
|
65
|
+
sys.exit(run_batch(args))
|
|
66
|
+
else:
|
|
67
|
+
root.print_help()
|
|
68
|
+
sys.exit(0)
|