Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/source/attacks/report.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,47 @@ Report

.. automodule:: sacroml.attacks.report
:members:

Converting legacy reports
-------------------------

Older SACRO-ML versions write a flat ``report.json`` keyed by
``"<attack_name>_<log_id>"``. The new reporting pipeline expects a nested,
catalog-enriched document validated by the bundled JSON schema
(``sacroml/reporting/sacroml_attack_report.schema.json``).

Use the ``convert-report`` command to upgrade an existing report in place
without re-running any attacks:

.. prompt:: bash

sacroml convert-report report.json report_new.json

The converter:

* wraps the legacy experiments under a top-level ``attacks`` key;
* injects the four human-readable catalogs (``metric_catalog``,
``parameter_catalog``, ``attack_category_catalog``, ``attack_catalog``)
from the bundled common definitions in
``sacroml/reporting/catalog_definitions.json``;
* **warns** when a metric, parameter, attack or attack category observed in
the report is not present in the catalogs (conversion still succeeds); and
* validates the result against the JSON schema.

Two small normalisations keep real-world legacy reports schema-valid: a
placeholder ``sacroml_version`` is injected when missing, and an empty
``attack_instance_logger`` is supplied for instance-less attacks (e.g.
structural attacks). Anything else that does not match the schema -- such as a
non-scalar metric value or a stray metadata key -- is reported as a schema
error rather than silently rewritten.

Curve-valued arrays (``fpr`` / ``tpr`` / ``roc_thresh``) are passed through
unchanged. ``roc_thresh`` legitimately starts with ``null``, which the schema
does not yet permit, so such violations are reported as notices rather than
errors. Pass ``--no-validate`` to skip schema validation.

To extend the catalogs with site-specific metrics, parameters or attacks,
edit ``sacroml/reporting/catalog_definitions.json``.

.. automodule:: sacroml.reporting.convert
:members: convert_report, convert_report_file, ConversionResult
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies = [
"dictdiffer",
"torch",
"prompt-toolkit",
"jsonschema",
]

[tool.setuptools.dynamic]
Expand Down Expand Up @@ -87,6 +88,10 @@ packages = {find = {exclude = ["docs*", "examples*", "tests*", "user_stories*"]}

[tool.setuptools.package-data]
"sacroml.safemodel" = ["rules.json"]
"sacroml.reporting" = [
"sacroml_attack_report.schema.json",
"catalog_definitions.json",
]

[tool.ruff]
indent-width = 4
Expand Down
61 changes: 61 additions & 0 deletions sacroml/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,58 @@
from __future__ import annotations

import argparse
import json
import os
import sys

from sacroml.attacks.factory import run_attacks
from sacroml.config.attack import prompt_for_attack
from sacroml.config.target import prompt_for_target
from sacroml.reporting import convert_report_file


def _run_convert_report(args: argparse.Namespace) -> int:
"""Convert a legacy report.json into the new report format.

Returns
-------
int
Process exit code (0 on success, 1 if the output is schema-invalid).
"""
if not os.path.isfile(args.input):
print(f"Input report not found: {args.input}")
return 1

try:
result = convert_report_file(
args.input, args.output, validate=not args.no_validate
)
except json.JSONDecodeError as exc:
print(f"Could not parse '{args.input}' as JSON: {exc}")
return 1
except OSError as exc:
print(f"Could not read or write report: {exc}")
return 1

print(f"Converted '{args.input}' -> '{args.output}'")
for dim, missing in result.coverage.items():
if missing:
print(f" {len(missing)} uncatalogued {dim.replace('_', ' ')}: {missing}")
if result.curve_warnings:
print(
f" {len(result.curve_warnings)} curve-array notice(s); "
"fpr/tpr/roc_thresh passed through unchanged."
)
if args.no_validate:
return 0
if result.is_valid:
print("Converted report is schema-valid.")
return 0
n_errors = len(result.schema_errors)
print(f"Converted report is NOT schema-valid ({n_errors} error(s)):")
for err in result.schema_errors:
print(f" - {err}")
return 1


def main() -> None:
Expand All @@ -26,6 +73,18 @@ def main() -> None:
subparsers.add_parser("gen-target", help="Generate Target YAML config")
subparsers.add_parser("gen-attack", help="Generate Attack YAML config")

convert = subparsers.add_parser(
"convert-report",
help="Convert a legacy report.json into the new report format",
)
convert.add_argument("input", type=str, help="Path to the legacy report.json")
convert.add_argument("output", type=str, help="Path to write the new report")
convert.add_argument(
"--no-validate",
action="store_true",
help="Skip JSON schema validation of the converted report",
)

args = parser.parse_args()

if args.cmd == "run":
Expand All @@ -37,6 +96,8 @@ def main() -> None:
prompt_for_target()
elif args.cmd == "gen-attack":
prompt_for_attack()
elif args.cmd == "convert-report":
sys.exit(_run_convert_report(args))


if __name__ == "__main__": # pragma:no cover
Expand Down
21 changes: 21 additions & 0 deletions sacroml/reporting/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Reporting utilities for SACRO-ML attack reports.

This subpackage contains the canonical attack-report JSON schema, a set of
common catalog definitions, and tooling to convert legacy ``report.json``
files produced by older SACRO-ML versions into the new nested,
catalog-enriched report format.
"""

from __future__ import annotations

from sacroml.reporting.convert import (
ConversionResult,
convert_report,
convert_report_file,
)

__all__ = [
"ConversionResult",
"convert_report",
"convert_report_file",
]
Loading