flagwake 0.5.0rc1__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.
Files changed (56) hide show
  1. cyberarena/__init__.py +8 -0
  2. cyberarena/cli.py +210 -0
  3. cyberarena/rc/baselines.py +702 -0
  4. cyberarena/rc/cli.py +580 -0
  5. cyberarena/rc/company.py +788 -0
  6. cyberarena/rc/conformance.py +329 -0
  7. cyberarena/rc/data/world.json +97 -0
  8. cyberarena/rc/data/worlds/fenwick-brae.json +1993 -0
  9. cyberarena/rc/data/worlds/harlow-pine.json +1045 -0
  10. cyberarena/rc/demo.py +208 -0
  11. cyberarena/rc/doctor.py +273 -0
  12. cyberarena/rc/engine.py +1354 -0
  13. cyberarena/rc/gen2.py +1702 -0
  14. cyberarena/rc/generator.py +1146 -0
  15. cyberarena/rc/golden.py +93 -0
  16. cyberarena/rc/html.py +403 -0
  17. cyberarena/rc/mcp.py +240 -0
  18. cyberarena/rc/mcp_client.py +162 -0
  19. cyberarena/rc/metrics.py +277 -0
  20. cyberarena/rc/model_agent.py +493 -0
  21. cyberarena/rc/packs.py +90 -0
  22. cyberarena/rc/profile.py +498 -0
  23. cyberarena/rc/protocol.py +477 -0
  24. cyberarena/rc/registry.py +678 -0
  25. cyberarena/rc/registry_cli.py +551 -0
  26. cyberarena/rc/report.py +551 -0
  27. cyberarena/rc/rules.py +132 -0
  28. cyberarena/rc/runner.py +232 -0
  29. cyberarena/rc/sanity.py +391 -0
  30. cyberarena/rc/sanity2.py +207 -0
  31. cyberarena/rc/scoring.py +867 -0
  32. cyberarena/rc/serve.py +92 -0
  33. cyberarena/rc/text.py +122 -0
  34. cyberarena/rc/text2.py +443 -0
  35. cyberarena/rc/tools_cli.py +183 -0
  36. cyberarena/rc/toolsapi.py +517 -0
  37. cyberarena/rc/toolsrun.py +601 -0
  38. cyberarena/rc/ui.py +1076 -0
  39. cyberarena/rc/ui_text.py +376 -0
  40. cyberarena/rc/verify.py +285 -0
  41. cyberarena/rc/web/css/rc.css +31 -0
  42. cyberarena/rc/web/css/ui.css +359 -0
  43. cyberarena/rc/web/js/ui.js +756 -0
  44. cyberarena/rc/web/js/viewer.js +186 -0
  45. cyberarena/rc/web/ui.html +279 -0
  46. cyberarena/rc/web/viewer.html +28 -0
  47. cyberarena/rc/world.py +267 -0
  48. cyberarena/rc/world_cli.py +390 -0
  49. flagwake-0.5.0rc1.dist-info/METADATA +71 -0
  50. flagwake-0.5.0rc1.dist-info/RECORD +56 -0
  51. flagwake-0.5.0rc1.dist-info/WHEEL +5 -0
  52. flagwake-0.5.0rc1.dist-info/entry_points.txt +2 -0
  53. flagwake-0.5.0rc1.dist-info/licenses/LICENSE +661 -0
  54. flagwake-0.5.0rc1.dist-info/licenses/LICENSE-DATA +417 -0
  55. flagwake-0.5.0rc1.dist-info/licenses/NOTICE +15 -0
  56. flagwake-0.5.0rc1.dist-info/top_level.txt +1 -0
cyberarena/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Flagwake Responder Check: a re-checkable test of how an AI defender behaves during a security
2
+ incident, scored with business cost. The code lives in ``cyberarena.rc``.
3
+
4
+ The package keeps its historical name ``cyberarena``. The generator's random-stream labels start
5
+ with the same word (``cyberarena/rc/1/...``) and must never change: every incident depends on them.
6
+ """
7
+
8
+ __version__ = "0.5.0rc1"
cyberarena/cli.py ADDED
@@ -0,0 +1,210 @@
1
+ """Command-line entry point of ``flagwake``.
2
+
3
+ The Responder Check commands (``check``, ``baselines``, ``replay``, ``doctor``, ``mcp``,
4
+ ``serve``) are registered from ``cyberarena.rc.cli``, the registry commands (``init``,
5
+ ``defenders``, ``run``, ``view``) from ``cyberarena.rc.registry_cli``; ``verify`` and ``report``
6
+ are defined here.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import textwrap
14
+ from collections.abc import Sequence
15
+
16
+ from cyberarena import __version__
17
+
18
+ DESCRIPTION = (
19
+ "Flagwake Responder Check: a free, offline, re-checkable test of how an AI defender behaves "
20
+ "during a security incident, scored with business cost."
21
+ )
22
+ # The commands as `flagwake --help` lists them: what a newcomer needs first, then the rest.
23
+ GROUPS: tuple[tuple[str, tuple[tuple[str, str], ...]], ...] = (
24
+ ("Start here", (
25
+ ("demo", "see it work in a few seconds: two example defenders, a report, verify"),
26
+ ("ui", "the local page: add a defender, check it, run it, read the result"),
27
+ )),
28
+ ("Test a defender", (
29
+ ("init", "create flagwake.yaml, the list of defenders, in this folder"),
30
+ ("defenders", "add, list, show, remove or test a defender"),
31
+ ("run", "run registered defenders on fresh incidents and write reports"),
32
+ ("verify", "re-derive every number of a report.json from its stored commands"),
33
+ )),
34
+ ("Read results", (
35
+ ("view", "open the newest report in the report viewer"),
36
+ ("replay", "print one incident of a report as a plain transcript"),
37
+ ("baselines", "run the shipped reference programs and print a table"),
38
+ )),
39
+ ("Your own company", (
40
+ ("world", "write, check or show a company file"),
41
+ )),
42
+ ("Other ways in", (
43
+ ("check", "a one-off run of a command or endpoint, with no registry"),
44
+ ("doctor", "a one-minute smoke test of a model endpoint or a program"),
45
+ ("tools", "serve the company as tools (MCP and REST) to a product with its own loop"),
46
+ ("mcp", "the check as a stdio MCP server (score its logs with report)"),
47
+ ("report", "score MCP transcripts into report.json and report.html"),
48
+ ("serve", "serve the report viewer on 127.0.0.1"),
49
+ )),
50
+ ) # fmt: skip
51
+
52
+
53
+ def overview() -> str:
54
+ """What `flagwake --help` prints: the commands in the order a newcomer needs them."""
55
+ lines = ["usage: flagwake <command> [options]", "", *textwrap.wrap(DESCRIPTION, 92), ""]
56
+ for title, commands in GROUPS:
57
+ lines.append(f"{title}:")
58
+ lines += [f" {name:<11}{text}" for name, text in commands]
59
+ lines.append("")
60
+ lines += [
61
+ "flagwake <command> --help shows a command's options; flagwake --version the version.",
62
+ "Docs: README.md, then docs/QUICKSTART.md.",
63
+ ]
64
+ return "\n".join(lines) + "\n"
65
+
66
+
67
+ class _TopParser(argparse.ArgumentParser):
68
+ """The top-level parser: its help is the grouped overview, not one flat list."""
69
+
70
+ def format_help(self) -> str:
71
+ return overview()
72
+
73
+
74
+ REPORT_SCHEMA = "flagwake-rc-report-1"
75
+
76
+
77
+ def build_parser() -> argparse.ArgumentParser:
78
+ """Return the top-level argument parser."""
79
+ parser = _TopParser(prog="flagwake", description=DESCRIPTION)
80
+ parser.add_argument("--version", action="version", version=f"flagwake {__version__}")
81
+ sub = parser.add_subparsers(
82
+ dest="command", metavar="<command>", parser_class=argparse.ArgumentParser
83
+ )
84
+
85
+ from cyberarena.rc.cli import add_subparsers as _rc_add
86
+
87
+ _rc_add(sub)
88
+ from cyberarena.rc.world_cli import add_subparsers as _world_add
89
+
90
+ _world_add(sub)
91
+ from cyberarena.rc.tools_cli import add_subparsers as _tools_add
92
+
93
+ _tools_add(sub)
94
+
95
+ v = sub.add_parser(
96
+ "verify", help="re-derive every number of a report.json from its transcripts"
97
+ )
98
+ v.add_argument("files", nargs="+")
99
+ v.add_argument("--world", help="the company file a custom-world report was made from")
100
+
101
+ d = sub.add_parser(
102
+ "demo",
103
+ help="see it work in a few seconds: two example defenders, a report, verify, and an "
104
+ "edited copy that verify catches",
105
+ )
106
+ d.add_argument("--out", default="flagwake-demo", help="folder to write (default flagwake-demo)")
107
+ d.add_argument("--open", action="store_true", help="open the HTML report in your browser")
108
+
109
+ rp = sub.add_parser("report", help="score MCP transcripts into report.json and report.html")
110
+ rp.add_argument(
111
+ "--mcp-log",
112
+ required=True,
113
+ help="the folder of transcripts written by flagwake mcp --log-dir",
114
+ )
115
+ rp.add_argument("--out", help="the folder to write (default mcp-report)")
116
+ rp.add_argument("--world", help="the company file the transcripts ran in, when it was a file")
117
+ return parser
118
+
119
+
120
+ def _verify_one(path: str, world: str | None = None) -> tuple[list[str], str | None]:
121
+ try:
122
+ with open(path, encoding="utf-8") as f:
123
+ doc = json.load(f)
124
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError) as exc:
125
+ return [f"cannot read: {exc}"], None
126
+ from cyberarena.rc.verify import made_by, verify
127
+
128
+ return verify(doc, world), made_by(doc)
129
+
130
+
131
+ def _cmd_verify(args: argparse.Namespace) -> int:
132
+ failed = 0
133
+ for path in args.files:
134
+ errors, note = _verify_one(path, getattr(args, "world", None))
135
+ if errors:
136
+ failed += 1
137
+ print(f"FAIL {path}: " + "; ".join(errors))
138
+ else:
139
+ print(f"OK {path}" + (f" ({note})" if note else ""))
140
+ return 1 if failed else 0
141
+
142
+
143
+ def _rc(handler_name: str):
144
+ def run(args: argparse.Namespace) -> int:
145
+ from cyberarena.rc import cli as rc_cli
146
+
147
+ return getattr(rc_cli, handler_name)(args)
148
+
149
+ return run
150
+
151
+
152
+ def _registry(handler_name: str):
153
+ def run(args: argparse.Namespace) -> int:
154
+ from cyberarena.rc import registry_cli
155
+
156
+ return getattr(registry_cli, handler_name)(args)
157
+
158
+ return run
159
+
160
+
161
+ def _cmd_demo(args: argparse.Namespace) -> int:
162
+ from cyberarena.rc.demo import cmd_demo
163
+
164
+ return cmd_demo(args)
165
+
166
+
167
+ def _cmd_tools(args: argparse.Namespace) -> int:
168
+ from cyberarena.rc import tools_cli
169
+
170
+ return tools_cli.cmd_tools(args)
171
+
172
+
173
+ def _cmd_world(args: argparse.Namespace) -> int:
174
+ from cyberarena.rc import world_cli
175
+
176
+ return world_cli.cmd_world(args)
177
+
178
+
179
+ COMMANDS = {
180
+ "verify": _cmd_verify,
181
+ "demo": _cmd_demo,
182
+ "report": _rc("cmd_report_mcp"),
183
+ "check": _rc("cmd_check"),
184
+ "doctor": _rc("cmd_doctor"),
185
+ "replay": _rc("cmd_replay"),
186
+ "baselines": _rc("cmd_baselines"),
187
+ "mcp": _rc("cmd_mcp"),
188
+ "serve": _rc("cmd_serve"),
189
+ "init": _registry("cmd_init"),
190
+ "defenders": _registry("cmd_defenders"),
191
+ "run": _registry("cmd_run"),
192
+ "view": _registry("cmd_view"),
193
+ "ui": _registry("cmd_ui"),
194
+ "world": _cmd_world,
195
+ "tools": _cmd_tools,
196
+ }
197
+
198
+
199
+ def main(argv: Sequence[str] | None = None) -> int:
200
+ """Run the CLI and return a process exit code."""
201
+ parser = build_parser()
202
+ args = parser.parse_args(argv)
203
+ if args.command in COMMANDS:
204
+ return COMMANDS[args.command](args)
205
+ parser.print_help()
206
+ return 0
207
+
208
+
209
+ if __name__ == "__main__":
210
+ raise SystemExit(main())