Skip to content

Commit 7aa344f

Browse files
authored
Feature/reporting categories (#389)
* add reporting top-level config object, and taxonomy variable for it * move report_dir config into reporting config * move report_prefix config item to reporting config * update report html to support flexible grouping by top-level tag * presentation tweaks, add second-order sorts
1 parent 78f0c66 commit 7aa344f

File tree

10 files changed

+107
-47
lines changed

10 files changed

+107
-47
lines changed

garak/_config.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@
1818

1919
version = -1 # eh why this is here? hm. who references it
2020

21-
system_params = (
22-
"verbose report_prefix narrow_output parallel_requests parallel_attempts".split()
23-
)
21+
system_params = "verbose narrow_output parallel_requests parallel_attempts".split()
2422
run_params = "seed deprefix eval_threshold generations probe_tags".split()
2523
plugins_params = "model_type model_name extended_detectors".split()
24+
reporting_params = "taxonomy report_prefix".split()
2625

2726

2827
loaded = False
@@ -45,19 +44,22 @@ class TransientConfig(GarakSubConfig):
4544
basedir = pathlib.Path(__file__).parents[0]
4645
starttime = None
4746
starttime_iso = None
48-
report_dir = "runs"
4947

5048

5149
transient = TransientConfig()
5250

5351
system = GarakSubConfig()
5452
run = GarakSubConfig()
5553
plugins = GarakSubConfig()
54+
reporting = GarakSubConfig()
5655
plugins.probes = {}
5756
plugins.generators = {}
5857
plugins.detectors = {}
5958
plugins.buffs = {}
6059
plugins.harnesses = {}
60+
reporting.report_dir = "runs"
61+
reporting.taxonomy = None # set here to enable report_digest to be called directly
62+
6163

6264
config_files = []
6365

@@ -95,11 +97,12 @@ def _load_yaml_config(settings_filenames) -> dict:
9597

9698

9799
def _store_config(settings_files) -> None:
98-
global system, run, plugins
100+
global system, run, plugins, reporting
99101
settings = _load_yaml_config(settings_files)
100102
system = _set_settings(system, settings["system"])
101103
run = _set_settings(run, settings["run"])
102104
plugins = _set_settings(plugins, settings["plugins"])
105+
reporting = _set_settings(reporting, settings["reporting"])
103106

104107

105108
def load_base_config() -> None:

garak/analyze/report_digest.py

+55-21
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
import jinja2
1313

14+
from garak import _config, _plugins
15+
1416
templateLoader = jinja2.FileSystemLoader(searchpath=".")
1517
templateEnv = jinja2.Environment(loader=templateLoader)
1618

@@ -20,9 +22,7 @@
2022
footer_template = templateEnv.get_template(
2123
"garak/analyze/templates/digest_footer.jinja"
2224
)
23-
module_template = templateEnv.get_template(
24-
"garak/analyze/templates/digest_module.jinja"
25-
)
25+
group_template = templateEnv.get_template("garak/analyze/templates/digest_group.jinja")
2626
probe_template = templateEnv.get_template("garak/analyze/templates/digest_probe.jinja")
2727
detector_template = templateEnv.get_template(
2828
"garak/analyze/templates/digest_detector.jinja"
@@ -43,7 +43,7 @@ def map_score(score):
4343
return 4
4444

4545

46-
def compile_digest(report_path):
46+
def compile_digest(report_path, taxonomy=_config.reporting.taxonomy):
4747
evals = []
4848
setup = defaultdict(str)
4949
with open(report_path, "r", encoding="utf-8") as reportfile:
@@ -78,6 +78,7 @@ def compile_digest(report_path):
7878

7979
create_table = """create table results(
8080
probe_module VARCHAR(255) not null,
81+
probe_group VARCHAR(255) not null,
8182
probe_class VARCHAR(255) not null,
8283
detector VARCHAR(255) not null,
8384
score FLOAT not null,
@@ -92,22 +93,38 @@ def compile_digest(report_path):
9293
detector = eval["detector"].replace("detector.", "")
9394
score = eval["passed"] / eval["total"]
9495
instances = eval["total"]
95-
cursor.execute(
96-
f"insert into results values ('{pm}', '{pc}', '{detector}', '{score}', '{instances}')"
97-
)
96+
groups = []
97+
if taxonomy is not None:
98+
# get the probe tags
99+
m = importlib.import_module(f"garak.probes.{pm}")
100+
tags = getattr(m, pc).tags
101+
for tag in tags:
102+
if tag.split(":")[0] == taxonomy:
103+
groups.append(":".join(tag.split(":")[1:]))
104+
if groups == []:
105+
groups = ["other"]
106+
else:
107+
groups = [pm]
108+
# add a row for each group
109+
for group in groups:
110+
cursor.execute(
111+
f"insert into results values ('{pm}', '{group}', '{pc}', '{detector}', '{score}', '{instances}')"
112+
)
98113

99114
# calculate per-probe scores
100115

101-
res = cursor.execute("select distinct probe_module from results")
102-
module_names = [i[0] for i in res.fetchall()]
116+
res = cursor.execute(
117+
"select distinct probe_group from results order by probe_group"
118+
)
119+
group_names = [i[0] for i in res.fetchall()]
103120

104121
# top score: % of passed probes
105122
# probe score: mean of detector scores
106123

107124
# let's build a dict of per-probe score
108125

109-
for probe_module in module_names:
110-
sql = f"select avg(score)*100 as s from results where probe_module = '{probe_module}' order by s asc;"
126+
for probe_group in group_names:
127+
sql = f"select avg(score)*100 as s from results where probe_group = '{probe_group}' order by s asc, probe_class asc;"
111128
# sql = f"select probe_module || '.' || probe_class, avg(score) as s from results where probe_module = '{probe_module}' group by probe_module, probe_class order by desc(s)"
112129
res = cursor.execute(sql)
113130
# probe_scores = res.fetchall()
@@ -116,24 +133,38 @@ def compile_digest(report_path):
116133
# top_score = passing_probe_count / probe_count
117134
top_score = res.fetchone()[0]
118135

119-
probe_module = re.sub("[^0-9A-Za-z_]", "", probe_module)
120-
m = importlib.import_module(f"garak.probes.{probe_module}")
121-
module_doc = markdown.markdown(m.__doc__)
136+
group_doc = f"Probes tagged {probe_group}"
137+
group_link = ""
138+
139+
probe_group_name = probe_group
140+
if taxonomy is None:
141+
probe_module = re.sub("[^0-9A-Za-z_]", "", probe_group)
142+
m = importlib.import_module(f"garak.probes.{probe_module}")
143+
group_doc = markdown.markdown(m.__doc__)
144+
group_link = (
145+
f"https://reference.garak.ai/en/latest/garak.probes.{probe_group}.html"
146+
)
147+
elif probe_group != "other":
148+
probe_group_name = f"{taxonomy}:{probe_group}"
149+
else:
150+
probe_group_name = "Uncategorized"
122151

123-
digest_content += module_template.render(
152+
digest_content += group_template.render(
124153
{
125-
"module": probe_module,
154+
"module": probe_group_name,
126155
"module_score": f"{top_score:.1f}%",
127156
"severity": map_score(top_score),
128-
"module_doc": module_doc,
157+
"module_doc": group_doc,
158+
"group_link": group_link,
129159
}
130160
)
131161

132162
if top_score < 100.0:
133163
res = cursor.execute(
134-
f"select probe_class, avg(score)*100 as s from results where probe_module='{probe_module}' group by probe_class order by s asc;"
164+
f"select probe_module, probe_class, avg(score)*100 as s from results where probe_group='{probe_group}' group by probe_class order by s asc, probe_class asc;"
135165
)
136-
for probe_class, score in res.fetchall():
166+
for probe_module, probe_class, score in res.fetchall():
167+
m = importlib.import_module(f"garak.probes.{probe_module}")
137168
digest_content += probe_template.render(
138169
{
139170
"plugin_name": probe_class,
@@ -145,7 +176,7 @@ def compile_digest(report_path):
145176
# print(f"\tplugin: {probe_module}.{probe_class} - {score:.1f}%")
146177
if score < 100.0:
147178
res = cursor.execute(
148-
f"select detector, score*100 from results where probe_module='{probe_module}' and probe_class='{probe_class}' order by score asc;"
179+
f"select detector, score*100 from results where probe_group='{probe_group}' and probe_class='{probe_class}' order by score asc, detector asc;"
149180
)
150181
for detector, score in res.fetchall():
151182
detector = re.sub(r"[^0-9A-Za-z_.]", "", detector)
@@ -176,5 +207,8 @@ def compile_digest(report_path):
176207

177208
if __name__ == "__main__":
178209
report_path = sys.argv[1]
179-
digest_content = compile_digest(report_path)
210+
taxonomy = None
211+
if len(sys.argv) == 3:
212+
taxonomy = sys.argv[2]
213+
digest_content = compile_digest(report_path, taxonomy=taxonomy)
180214
print(digest_content)
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
<button class="defcon{{severity}} accordion">{{ module }} - {{ module_score }}</button>
3+
<div class="panel">
4+
<p>{{module_doc}}</p>
5+
{%if module_score != "100.0%"%}
6+
<ul><li>Probes under
7+
{%if group_link%}
8+
<a href="{{group_link}}" target="_new">{{module}}</a>
9+
{%else%}
10+
"{{module}}"
11+
{%endif%}
12+
scored the system a {{ module_score }} pass rate.</li></ul>
13+
{%endif%}

garak/analyze/templates/digest_module.jinja

-7
This file was deleted.
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<h3 class="defcon{{severity}}">probe: {{ plugin_name }} {{ plugin_score }}</h3>
22
{%if plugin_score != "100.0%"%}
3-
<p class="probe">Tests for: {{plugin_descr}}</p>
3+
<p class="probe">Tests description: {{plugin_descr}}</p>
44
<p class="probe">{{plugin_name}}: <b>{{ plugin_score }}</b> resilient.</p>
55
{%endif%}

garak/cli.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def main(arguments=[]) -> None:
3232
import argparse
3333

3434
parser = argparse.ArgumentParser(
35+
prog="python -m garak",
3536
description="LLM safety & security scanning tool",
3637
epilog="See https://github.com/leondz/garak",
3738
)
@@ -47,7 +48,7 @@ def main(arguments=[]) -> None:
4748
parser.add_argument(
4849
"--report_prefix",
4950
type=str,
50-
default=_config.system.report_prefix,
51+
default=_config.reporting.report_prefix,
5152
help="Specify an optional prefix for the report and hit logs",
5253
)
5354
parser.add_argument(
@@ -173,6 +174,14 @@ def main(arguments=[]) -> None:
173174
help="buff to use",
174175
)
175176

177+
## REPORTING
178+
parser.add_argument(
179+
"--taxonomy",
180+
type=str,
181+
default=_config.reporting.taxonomy,
182+
help="specify a MISP top-level taxonomy to be used for grouping probes in reporting. e.g. 'avid-effect', 'owasp' ",
183+
)
184+
176185
## COMMANDS
177186
# items placed here also need to be listed in command_options below
178187
parser.add_argument(
@@ -292,10 +301,13 @@ def main(arguments=[]) -> None:
292301
setattr(_config.run, param, value)
293302
elif param in _config.plugins_params:
294303
setattr(_config.plugins, param, value)
304+
elif param in _config.reporting_params:
305+
setattr(_config.reporting, param, value)
295306
else:
296307
ignored_params.append((param, value))
308+
logging.debug("non-config params: %s", ignored_params)
297309

298-
# put plguin spec into the _spec config value, if set at cli
310+
# put plugin spec into the _spec config value, if set at cli
299311
if "probes" in args:
300312
_config.plugins.probe_spec = args.probes
301313
if "detectors" in args:

garak/command.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,19 @@ def start_run():
4040
logging.info("started at %s", _config.transient.starttime_iso)
4141
# print("ASSIGN UUID", args)
4242
_config.transient.run_id = str(uuid.uuid4()) # uuid1 is safe but leaks host info
43-
if not _config.system.report_prefix:
44-
if not os.path.isdir(_config.transient.report_dir):
43+
if not _config.reporting.report_prefix:
44+
if not os.path.isdir(_config.reporting.report_dir):
4545
try:
46-
os.mkdir(_config.transient.report_dir)
46+
os.mkdir(_config.reporting.report_dir)
4747
except PermissionError as e:
4848
raise PermissionError(
4949
"Can't create logging directory %s, quitting",
50-
_config.transient.report_dir,
50+
_config.reporting.report_dir,
5151
) from e
52-
_config.transient.report_filename = f"{_config.transient.report_dir}/garak.{_config.transient.run_id}.report.jsonl"
52+
_config.transient.report_filename = f"{_config.reporting.report_dir}/garak.{_config.transient.run_id}.report.jsonl"
5353
else:
5454
_config.transient.report_filename = (
55-
_config.system.report_prefix + ".report.jsonl"
55+
_config.reporting.report_prefix + ".report.jsonl"
5656
)
5757
_config.transient.reportfile = open(
5858
_config.transient.report_filename, "w", buffering=1, encoding="utf-8"
@@ -70,7 +70,7 @@ def start_run():
7070
type(None),
7171
):
7272
setup_dict[f"_config.{k}"] = v
73-
for subset in "system transient run plugins".split():
73+
for subset in "system transient run plugins reporting".split():
7474
for k, v in getattr(_config, subset).__dict__.items():
7575
if k[:2] != "__" and type(v) in (
7676
str,

garak/evaluators/base.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ def evaluate(self, attempts: List[garak.attempt.Attempt]) -> None:
6464
for idx, score in enumerate(attempt.detector_results[detector]):
6565
if not self.test(score): # if we don't pass
6666
if not _config.transient.hitlogfile:
67-
if not _config.system.report_prefix:
68-
hitlog_filename = f"{_config.transient.report_dir}/garak.{_config.transient.run_id}.hitlog.jsonl"
67+
if not _config.reporting.report_prefix:
68+
hitlog_filename = f"{_config.reporting.report_dir}/garak.{_config.transient.run_id}.hitlog.jsonl"
6969
else:
7070
hitlog_filename = (
71-
_config.system.report_prefix + ".hitlog.jsonl"
71+
_config.reporting.report_prefix + ".hitlog.jsonl"
7272
)
7373
logging.info("hit log in %s", hitlog_filename)
7474
_config.transient.hitlogfile = open(

garak/resources/garak.core.yaml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
22
system:
33
verbose: 0
4-
report_prefix:
54
narrow_output: false
65
parallel_requests: false
76
parallel_attempts: false
@@ -28,3 +27,7 @@ plugins:
2827
buff_spec:
2928
buffs: {}
3029
harnesses: {}
30+
31+
reporting:
32+
report_prefix:
33+
taxonomy:

tests/test_config.py

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
param_locs[p] = "run"
5252
for p in garak._config.plugins_params:
5353
param_locs[p] = "plugins"
54+
for p in garak._config.reporting_params:
55+
param_locs[p] = "reporting"
5456

5557

5658
# test CLI assertions of each var

0 commit comments

Comments
 (0)