aisbom-cli 0.9.2__tar.gz → 0.10.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisbom-cli
3
- Version: 0.9.2
3
+ Version: 0.10.0
4
4
  Summary: An AI Supply Chain security tool that that detects Pickle bombs and generates CycloneDX SBOMs for Machine Learning models.
5
5
  License-File: LICENSE
6
6
  Author: Ajoy L
@@ -1,5 +1,6 @@
1
1
  import typer
2
2
  import json
3
+ import os
3
4
  import tomllib
4
5
  import importlib.metadata
5
6
  from enum import Enum
@@ -29,8 +30,38 @@ console = Console()
29
30
 
30
31
 
31
32
  @app.callback(invoke_without_command=True)
32
- def main(ctx: typer.Context):
33
- """AIsbom — AI Supply Chain Security Scanner."""
33
+ def main(
34
+ ctx: typer.Context,
35
+ version: bool = typer.Option(
36
+ False,
37
+ "--version",
38
+ "-V",
39
+ help="Print the installed version and exit.",
40
+ is_eager=True,
41
+ ),
42
+ ):
43
+ """
44
+ AIsbom — AI Supply Chain Security Scanner.
45
+
46
+ Deep introspection of ML model artifacts (.pt, .safetensors, .gguf) for
47
+ malware, license risk, and silent drift. Run `aisbom` with no args to see
48
+ a working example, or `aisbom <command> --help` for details on each command.
49
+
50
+ Environment variables:
51
+
52
+ AISBOM_NO_TELEMETRY=1 Disable all anonymous usage telemetry. Honored on
53
+ every code path; never overridden.
54
+ """
55
+ # Order matters: --version wins over the no-args panel so that
56
+ # `aisbom --version` is short and scriptable.
57
+ if version:
58
+ try:
59
+ ver = importlib.metadata.version("aisbom-cli")
60
+ except importlib.metadata.PackageNotFoundError:
61
+ ver = "unknown (dev build)"
62
+ console.print(f"aisbom {ver}")
63
+ raise typer.Exit(code=0)
64
+
34
65
  # Owns the no-args codepath. When the user runs `aisbom` with no
35
66
  # subcommand, show a one-screen quickstart instead of Typer's auto-help
36
67
  # block. `--help` still short-circuits to Typer's full reference, and
@@ -131,6 +162,69 @@ def _flush_telemetry_threads(threads: list[threading.Thread | None]) -> None:
131
162
  t.join(timeout=2.0)
132
163
 
133
164
 
165
+ def _attribution_ref(base_url: str) -> str:
166
+ """Append `ref=cli` to a URL so we can attribute return visits to CLI
167
+ users in GA4 Acquisition reports. Strips the tag when the user has opted
168
+ out of telemetry — the URL stays useful, just untracked."""
169
+ if os.getenv("AISBOM_NO_TELEMETRY"):
170
+ return base_url
171
+ sep = "&" if "?" in base_url else "?"
172
+ return f"{base_url}{sep}ref=cli"
173
+
174
+
175
+ def _render_scan_footer(
176
+ *,
177
+ share_url: str | None,
178
+ output_path: str | None,
179
+ output_format: "OutputFormat",
180
+ share_attempted: bool,
181
+ ) -> None:
182
+ """Print the post-scan acquisition footer (Phase 4.3).
183
+
184
+ Drives recurring engagement with aisbom.io properties — the viewer
185
+ (per-scan) and the advisories page (weekly). Three flavors:
186
+
187
+ 1. `--share` succeeded → point at the hosted viewer URL.
188
+ 2. `--share` not used (or failed) and machine-readable format → drag-and-
189
+ drop hint + a nudge to try `--share` next time.
190
+ 3. Markdown format → only the advisories link (no viewer hint, since
191
+ the markdown report isn't a viewer input).
192
+
193
+ URL attribution respects `AISBOM_NO_TELEMETRY` via `_attribution_ref`.
194
+ """
195
+ lines: list[str] = []
196
+
197
+ if share_url:
198
+ lines.append(
199
+ f"🔗 [bold]View this SBOM online:[/bold] "
200
+ f"[underline cyan]{_attribution_ref(share_url)}[/underline cyan]"
201
+ )
202
+ elif output_format in (OutputFormat.JSON, OutputFormat.SPDX) and output_path:
203
+ lines.append(
204
+ f"📊 [bold]Drag [cyan]{output_path}[/cyan] into the offline viewer:[/bold]\n"
205
+ f" [underline cyan]{_attribution_ref('https://aisbom.io/viewer')}[/underline cyan]"
206
+ )
207
+ if not share_attempted:
208
+ lines.append(
209
+ "💡 [dim]Tip: re-run with [white]--share[/white] to get a hosted viewer link.[/dim]"
210
+ )
211
+
212
+ # Always: advisories. Recurring re-engagement vector independent of format.
213
+ lines.append(
214
+ f"📰 [bold]Latest model advisories:[/bold] "
215
+ f"[underline cyan]{_attribution_ref('https://aisbom.io/advisories')}[/underline cyan]"
216
+ )
217
+
218
+ console.print(
219
+ Panel(
220
+ "\n".join(lines),
221
+ title=" Next steps ",
222
+ border_style="blue",
223
+ expand=False,
224
+ )
225
+ )
226
+
227
+
134
228
  class OutputFormat(str, Enum):
135
229
  JSON = "json"
136
230
  MARKDOWN = "markdown"
@@ -171,7 +265,13 @@ def _generate_markdown(results: dict) -> str:
171
265
 
172
266
  @app.command()
173
267
  def scan(
174
- target: str = typer.Argument(".", help="Directory or URL (http/hf://) to scan"),
268
+ target: str = typer.Argument(
269
+ ".",
270
+ help=(
271
+ "Local directory, HTTP(S) URL, or Hugging Face slug "
272
+ "(e.g. ./models, https://example.com/model.pt, hf://google-bert/bert-base-uncased)."
273
+ ),
274
+ ),
175
275
  output: str | None = typer.Option(None, help="Output file path"),
176
276
  schema_version: str = typer.Option("1.6", help="CycloneDX schema version (default is 1.6)", case_sensitive=False, rich_help_panel="Advanced Options"),
177
277
  spdx_version: str = typer.Option("2.3", help="SPDX version (2.3 or 3.0)", case_sensitive=False, rich_help_panel="Advanced Options"),
@@ -179,8 +279,23 @@ def scan(
179
279
  strict: bool = typer.Option(False, help="Enable strict allowlisting mode (flags any unknown imports)"),
180
280
  lint: bool = typer.Option(False, help="Enable Migration Linter (checks for weights_only=True compatibility)"),
181
281
  format: OutputFormat = typer.Option(OutputFormat.JSON, help="Output format (JSON for SBOM, MARKDOWN for Human Report, SPDX for Compliance)"),
182
- share: bool = typer.Option(False, help="Upload the SBOM to aisbom.io and generate a shareable viewer link"),
183
- share_yes: bool = typer.Option(False, "--share-yes", help="Skip confirmation prompt when using --share")
282
+ share: bool = typer.Option(
283
+ False,
284
+ help=(
285
+ "Upload the generated SBOM to aisbom.io and print a public viewer URL. "
286
+ "Anyone with the link can view the SBOM; data expires after 30 days. "
287
+ "Prompts for confirmation before uploading unless --share-yes is also set."
288
+ ),
289
+ ),
290
+ share_yes: bool = typer.Option(
291
+ False,
292
+ "--share-yes",
293
+ help=(
294
+ "Skip the --share confirmation prompt. Intended for CI/CD pipelines; "
295
+ "do not pass this flag interactively unless you understand that the "
296
+ "uploaded SBOM becomes publicly viewable for 30 days."
297
+ ),
298
+ ),
184
299
  ):
185
300
  """
186
301
  Deep Introspection Scan: Analyzes binary headers and dependency manifests.
@@ -199,6 +314,11 @@ def scan(
199
314
  _maybe_emit_install_event(),
200
315
  ]
201
316
 
317
+ # Phase 4.3 — captured by the --share success path below and read by the
318
+ # acquisition footer at scan end. Function-scope name so the inner JSON
319
+ # branch can write to it and the footer can read it.
320
+ share_url: str | None = None
321
+
202
322
  # 1. Run the Logic
203
323
  try:
204
324
  scanner = DeepScanner(target, strict_mode=strict, lint=lint)
@@ -425,20 +545,15 @@ def scan(
425
545
  f.write(markdown)
426
546
  console.print(f"\n[bold green]✔ Markdown Report Generated:[/bold green] {output}")
427
547
 
428
- # Show visualization panel for machine-readable formats
429
- if format in [OutputFormat.JSON, OutputFormat.SPDX]:
430
- try:
431
- ver = importlib.metadata.version("aisbom-cli")
432
- except importlib.metadata.PackageNotFoundError:
433
- ver = "unknown"
434
-
435
- console.print(Panel(
436
- f"[bold white]📊 Visualize this report:[/bold white]\n"
437
- f"Drag and drop [cyan]{output}[/cyan] into our secure offline viewer:\n"
438
- f"> View detailed artifact visualizer: https://aisbom.io/viewer",
439
- border_style="blue",
440
- expand=False
441
- ))
548
+ # Phase 4.3 acquisition footer (replaces the previous "Visualize this
549
+ # report" panel). Always shown after a successful scan. Drives recurring
550
+ # engagement with aisbom.io's viewer + advisories pages.
551
+ _render_scan_footer(
552
+ share_url=share_url,
553
+ output_path=output,
554
+ output_format=format,
555
+ share_attempted=share,
556
+ )
442
557
 
443
558
  # Signal exit behavior to the user
444
559
  if exit_code == 2:
@@ -466,12 +581,21 @@ def info():
466
581
  except importlib.metadata.PackageNotFoundError:
467
582
  ver = "unknown (dev build)"
468
583
 
584
+ # Phase 4 help-pass: surface telemetry state in `info` so users have one
585
+ # canonical place to confirm whether events are firing on their machine.
586
+ telemetry_state = (
587
+ "opted out via AISBOM_NO_TELEMETRY"
588
+ if os.getenv("AISBOM_NO_TELEMETRY")
589
+ else "enabled (set AISBOM_NO_TELEMETRY=1 to disable)"
590
+ )
591
+
469
592
  console.print(Panel(
470
593
  f"[bold cyan]AI SBOM[/bold cyan]: AI Software Bill of Materials - The Supply Chain for Artificial Intelligence\n"
471
594
  f"[bold]Version:[/bold] {ver}\n"
472
595
  f"[bold]License:[/bold] Apache 2.0\n"
473
596
  f"[bold]Website:[/bold] https://www.aisbom.io\n"
474
- f"[bold]Repository:[/bold] https://github.com/Lab700xOrg/aisbom",
597
+ f"[bold]Repository:[/bold] https://github.com/Lab700xOrg/aisbom\n"
598
+ f"[bold]Telemetry:[/bold] {telemetry_state}",
475
599
  title=" System Info ",
476
600
  border_style="magenta",
477
601
  expand=False
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "aisbom-cli"
3
- version = "0.9.2"
3
+ version = "0.10.0"
4
4
  description = "An AI Supply Chain security tool that that detects Pickle bombs and generates CycloneDX SBOMs for Machine Learning models."
5
5
  authors = ["Ajoy L <lab700xdev@gmail.com>"]
6
6
  readme = "README.md"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes