buildlog 0.5.0__py3-none-any.whl → 0.6.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.
- buildlog/cli.py +391 -3
- buildlog/data/__init__.py +0 -0
- buildlog/data/seeds/security_karen.yaml +162 -0
- buildlog/data/seeds/test_terrorist.yaml +280 -0
- buildlog/seed_engine/__init__.py +74 -0
- buildlog/seed_engine/categorizers.py +145 -0
- buildlog/seed_engine/extractors.py +148 -0
- buildlog/seed_engine/generators.py +144 -0
- buildlog/seed_engine/models.py +113 -0
- buildlog/seed_engine/pipeline.py +202 -0
- buildlog/seed_engine/sources.py +362 -0
- buildlog/seeds.py +261 -0
- buildlog/skills.py +26 -3
- {buildlog-0.5.0.dist-info → buildlog-0.6.1.dist-info}/METADATA +82 -11
- buildlog-0.6.1.dist-info/RECORD +41 -0
- buildlog-0.5.0.dist-info/RECORD +0 -30
- {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/copier.yml +0 -0
- {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/post_gen.py +0 -0
- {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
- {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
- {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
- {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
- {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
- {buildlog-0.5.0.dist-info → buildlog-0.6.1.dist-info}/WHEEL +0 -0
- {buildlog-0.5.0.dist-info → buildlog-0.6.1.dist-info}/entry_points.txt +0 -0
- {buildlog-0.5.0.dist-info → buildlog-0.6.1.dist-info}/licenses/LICENSE +0 -0
buildlog/cli.py
CHANGED
|
@@ -172,8 +172,8 @@ def new(slug: str, entry_date: str | None):
|
|
|
172
172
|
click.echo(f"\nOpen it: $EDITOR {entry_path}")
|
|
173
173
|
|
|
174
174
|
|
|
175
|
-
@main.command()
|
|
176
|
-
def
|
|
175
|
+
@main.command("list")
|
|
176
|
+
def list_entries():
|
|
177
177
|
"""List all buildlog entries."""
|
|
178
178
|
buildlog_dir = Path("buildlog")
|
|
179
179
|
|
|
@@ -182,7 +182,8 @@ def list():
|
|
|
182
182
|
raise SystemExit(1)
|
|
183
183
|
|
|
184
184
|
entries = sorted(
|
|
185
|
-
buildlog_dir.glob("20??-??-??-*.md"),
|
|
185
|
+
buildlog_dir.glob("20??-??-??-*.md"),
|
|
186
|
+
reverse=True, # Most recent first
|
|
186
187
|
)
|
|
187
188
|
|
|
188
189
|
if not entries:
|
|
@@ -876,5 +877,392 @@ def experiment_report(output_json: bool):
|
|
|
876
877
|
)
|
|
877
878
|
|
|
878
879
|
|
|
880
|
+
# -----------------------------------------------------------------------------
|
|
881
|
+
# Gauntlet Commands (Review Personas)
|
|
882
|
+
# -----------------------------------------------------------------------------
|
|
883
|
+
|
|
884
|
+
PERSONAS = {
|
|
885
|
+
"security_karen": "OWASP Top 10 security review",
|
|
886
|
+
"test_terrorist": "Comprehensive testing coverage audit",
|
|
887
|
+
"ruthless_reviewer": "Code quality and functional principles",
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
@main.group()
|
|
892
|
+
def gauntlet():
|
|
893
|
+
"""Run the review gauntlet with curated personas.
|
|
894
|
+
|
|
895
|
+
The gauntlet runs your code through multiple ruthless reviewers,
|
|
896
|
+
each with domain-specific rules loaded from seed files.
|
|
897
|
+
|
|
898
|
+
Personas:
|
|
899
|
+
- security_karen: OWASP security review (12 rules)
|
|
900
|
+
- test_terrorist: Testing coverage audit (21 rules)
|
|
901
|
+
- ruthless_reviewer: Code quality review (coming soon)
|
|
902
|
+
|
|
903
|
+
Example workflow:
|
|
904
|
+
|
|
905
|
+
buildlog gauntlet list # See available personas
|
|
906
|
+
buildlog gauntlet rules --persona all # Show all rules
|
|
907
|
+
buildlog gauntlet prompt src/ # Generate review prompt
|
|
908
|
+
"""
|
|
909
|
+
pass
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
@gauntlet.command("list")
|
|
913
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
914
|
+
def gauntlet_list(output_json: bool):
|
|
915
|
+
"""List available reviewer personas and their rule counts.
|
|
916
|
+
|
|
917
|
+
Examples:
|
|
918
|
+
|
|
919
|
+
buildlog gauntlet list
|
|
920
|
+
buildlog gauntlet list --json
|
|
921
|
+
"""
|
|
922
|
+
import json as json_module
|
|
923
|
+
|
|
924
|
+
from buildlog.seeds import get_default_seeds_dir, load_all_seeds
|
|
925
|
+
|
|
926
|
+
# Find seeds directory (local overrides > buildlog template > package bundled)
|
|
927
|
+
seeds_dir = get_default_seeds_dir()
|
|
928
|
+
|
|
929
|
+
if seeds_dir is None:
|
|
930
|
+
if output_json:
|
|
931
|
+
click.echo('{"personas": {}, "total_rules": 0, "error": "No seeds found"}')
|
|
932
|
+
else:
|
|
933
|
+
click.echo("No seed files found.")
|
|
934
|
+
click.echo("Seeds are bundled with buildlog - check your installation.")
|
|
935
|
+
return
|
|
936
|
+
|
|
937
|
+
seeds = load_all_seeds(seeds_dir)
|
|
938
|
+
|
|
939
|
+
if output_json:
|
|
940
|
+
data = {
|
|
941
|
+
"personas": {
|
|
942
|
+
name: {
|
|
943
|
+
"description": PERSONAS.get(name, "Custom persona"),
|
|
944
|
+
"rules_count": len(sf.rules),
|
|
945
|
+
"version": sf.version,
|
|
946
|
+
}
|
|
947
|
+
for name, sf in seeds.items()
|
|
948
|
+
},
|
|
949
|
+
"total_rules": sum(len(sf.rules) for sf in seeds.values()),
|
|
950
|
+
}
|
|
951
|
+
click.echo(json_module.dumps(data, indent=2))
|
|
952
|
+
else:
|
|
953
|
+
click.echo("Review Gauntlet Personas")
|
|
954
|
+
click.echo("=" * 50)
|
|
955
|
+
|
|
956
|
+
if not seeds:
|
|
957
|
+
click.echo("\nNo seed files found.")
|
|
958
|
+
click.echo("Initialize with: buildlog init")
|
|
959
|
+
click.echo("Or create seeds in: .buildlog/seeds/")
|
|
960
|
+
return
|
|
961
|
+
|
|
962
|
+
total = 0
|
|
963
|
+
for name, sf in sorted(seeds.items()):
|
|
964
|
+
desc = PERSONAS.get(name, "Custom persona")
|
|
965
|
+
click.echo(f"\n {name}")
|
|
966
|
+
click.echo(f" {desc}")
|
|
967
|
+
click.echo(f" Rules: {len(sf.rules)} (v{sf.version})")
|
|
968
|
+
total += len(sf.rules)
|
|
969
|
+
|
|
970
|
+
click.echo(f"\nTotal: {len(seeds)} personas, {total} rules")
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
@gauntlet.command("rules")
|
|
974
|
+
@click.option(
|
|
975
|
+
"--persona",
|
|
976
|
+
"-p",
|
|
977
|
+
default="all",
|
|
978
|
+
help="Persona to show rules for (or 'all')",
|
|
979
|
+
)
|
|
980
|
+
@click.option(
|
|
981
|
+
"--format",
|
|
982
|
+
"fmt",
|
|
983
|
+
type=click.Choice(["yaml", "json", "markdown"]),
|
|
984
|
+
default="yaml",
|
|
985
|
+
help="Output format",
|
|
986
|
+
)
|
|
987
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file")
|
|
988
|
+
def gauntlet_rules(persona: str, fmt: str, output: str | None):
|
|
989
|
+
"""Show rules for reviewer personas.
|
|
990
|
+
|
|
991
|
+
Use this to see what rules are loaded for each persona,
|
|
992
|
+
or export them for use in prompts.
|
|
993
|
+
|
|
994
|
+
Examples:
|
|
995
|
+
|
|
996
|
+
buildlog gauntlet rules # All rules (YAML)
|
|
997
|
+
buildlog gauntlet rules -p security_karen # Single persona
|
|
998
|
+
buildlog gauntlet rules --format json -o rules.json
|
|
999
|
+
buildlog gauntlet rules --format markdown # For docs
|
|
1000
|
+
"""
|
|
1001
|
+
import json as json_module
|
|
1002
|
+
|
|
1003
|
+
from buildlog.seeds import get_default_seeds_dir, load_all_seeds
|
|
1004
|
+
|
|
1005
|
+
# Find seeds directory (local overrides > buildlog template > package bundled)
|
|
1006
|
+
seeds_dir = get_default_seeds_dir()
|
|
1007
|
+
|
|
1008
|
+
if seeds_dir is None:
|
|
1009
|
+
click.echo("No seed files found.", err=True)
|
|
1010
|
+
click.echo(
|
|
1011
|
+
"Seeds are bundled with buildlog - check your installation.", err=True
|
|
1012
|
+
)
|
|
1013
|
+
raise SystemExit(1)
|
|
1014
|
+
|
|
1015
|
+
seeds = load_all_seeds(seeds_dir)
|
|
1016
|
+
|
|
1017
|
+
if not seeds:
|
|
1018
|
+
click.echo("No seed files found in directory.", err=True)
|
|
1019
|
+
raise SystemExit(1)
|
|
1020
|
+
|
|
1021
|
+
# Filter personas
|
|
1022
|
+
if persona != "all":
|
|
1023
|
+
if persona not in seeds:
|
|
1024
|
+
available = ", ".join(seeds.keys())
|
|
1025
|
+
click.echo(f"Unknown persona: {persona}", err=True)
|
|
1026
|
+
click.echo(f"Available: {available}", err=True)
|
|
1027
|
+
raise SystemExit(1)
|
|
1028
|
+
seeds = {persona: seeds[persona]}
|
|
1029
|
+
|
|
1030
|
+
# Build output data
|
|
1031
|
+
if fmt == "json":
|
|
1032
|
+
data = {}
|
|
1033
|
+
for name, sf in seeds.items():
|
|
1034
|
+
data[name] = {
|
|
1035
|
+
"version": sf.version,
|
|
1036
|
+
"rules": [
|
|
1037
|
+
{
|
|
1038
|
+
"rule": r.rule,
|
|
1039
|
+
"category": r.category,
|
|
1040
|
+
"context": r.context,
|
|
1041
|
+
"antipattern": r.antipattern,
|
|
1042
|
+
"rationale": r.rationale,
|
|
1043
|
+
"tags": r.tags,
|
|
1044
|
+
"references": [
|
|
1045
|
+
{"url": ref.url, "title": ref.title} for ref in r.references
|
|
1046
|
+
],
|
|
1047
|
+
}
|
|
1048
|
+
for r in sf.rules
|
|
1049
|
+
],
|
|
1050
|
+
}
|
|
1051
|
+
formatted = json_module.dumps(data, indent=2)
|
|
1052
|
+
|
|
1053
|
+
elif fmt == "markdown":
|
|
1054
|
+
lines = ["# Review Gauntlet Rules\n"]
|
|
1055
|
+
for name, sf in seeds.items():
|
|
1056
|
+
lines.append(f"## {name.replace('_', ' ').title()}\n")
|
|
1057
|
+
lines.append(f"*{len(sf.rules)} rules, v{sf.version}*\n")
|
|
1058
|
+
for i, r in enumerate(sf.rules, 1):
|
|
1059
|
+
lines.append(f"### {i}. {r.rule}\n")
|
|
1060
|
+
lines.append(f"**Category**: {r.category} ")
|
|
1061
|
+
lines.append(f"**Tags**: {', '.join(r.tags)}\n")
|
|
1062
|
+
if r.context:
|
|
1063
|
+
lines.append(f"**When**: {r.context}\n")
|
|
1064
|
+
if r.antipattern:
|
|
1065
|
+
lines.append(f"**Antipattern**: {r.antipattern}\n")
|
|
1066
|
+
if r.rationale:
|
|
1067
|
+
lines.append(f"**Why**: {r.rationale}\n")
|
|
1068
|
+
if r.references:
|
|
1069
|
+
lines.append("**References**:")
|
|
1070
|
+
for ref in r.references:
|
|
1071
|
+
lines.append(f"- [{ref.title}]({ref.url})")
|
|
1072
|
+
lines.append("")
|
|
1073
|
+
formatted = "\n".join(lines)
|
|
1074
|
+
|
|
1075
|
+
else: # yaml
|
|
1076
|
+
import yaml as yaml_module
|
|
1077
|
+
|
|
1078
|
+
data = {}
|
|
1079
|
+
for name, sf in seeds.items():
|
|
1080
|
+
data[name] = {
|
|
1081
|
+
"version": sf.version,
|
|
1082
|
+
"rules": [
|
|
1083
|
+
{
|
|
1084
|
+
"rule": r.rule,
|
|
1085
|
+
"category": r.category,
|
|
1086
|
+
"context": r.context,
|
|
1087
|
+
"antipattern": r.antipattern,
|
|
1088
|
+
"rationale": r.rationale,
|
|
1089
|
+
"tags": r.tags,
|
|
1090
|
+
}
|
|
1091
|
+
for r in sf.rules
|
|
1092
|
+
],
|
|
1093
|
+
}
|
|
1094
|
+
formatted = yaml_module.dump(data, default_flow_style=False, sort_keys=False)
|
|
1095
|
+
|
|
1096
|
+
# Output
|
|
1097
|
+
if output:
|
|
1098
|
+
output_path = Path(output)
|
|
1099
|
+
output_path.write_text(formatted, encoding="utf-8")
|
|
1100
|
+
total = sum(len(sf.rules) for sf in seeds.values())
|
|
1101
|
+
click.echo(f"Wrote {total} rules to {output_path}")
|
|
1102
|
+
else:
|
|
1103
|
+
click.echo(formatted)
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
@gauntlet.command("prompt")
|
|
1107
|
+
@click.argument("target", type=click.Path(exists=True))
|
|
1108
|
+
@click.option(
|
|
1109
|
+
"--persona",
|
|
1110
|
+
"-p",
|
|
1111
|
+
multiple=True,
|
|
1112
|
+
help="Personas to include (default: all)",
|
|
1113
|
+
)
|
|
1114
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file")
|
|
1115
|
+
def gauntlet_prompt(target: str, persona: tuple[str, ...], output: str | None):
|
|
1116
|
+
"""Generate a review prompt for the gauntlet.
|
|
1117
|
+
|
|
1118
|
+
Creates a prompt with rules and target code that can be
|
|
1119
|
+
used with Claude or another LLM to run a review.
|
|
1120
|
+
|
|
1121
|
+
Examples:
|
|
1122
|
+
|
|
1123
|
+
buildlog gauntlet prompt src/
|
|
1124
|
+
buildlog gauntlet prompt src/api.py -p security_karen
|
|
1125
|
+
buildlog gauntlet prompt . -o review_prompt.md
|
|
1126
|
+
"""
|
|
1127
|
+
from buildlog.seeds import get_default_seeds_dir, load_all_seeds
|
|
1128
|
+
|
|
1129
|
+
# Find seeds directory (local overrides > buildlog template > package bundled)
|
|
1130
|
+
seeds_dir = get_default_seeds_dir()
|
|
1131
|
+
|
|
1132
|
+
if seeds_dir is None:
|
|
1133
|
+
click.echo("No seed files found.", err=True)
|
|
1134
|
+
click.echo(
|
|
1135
|
+
"Seeds are bundled with buildlog - check your installation.", err=True
|
|
1136
|
+
)
|
|
1137
|
+
raise SystemExit(1)
|
|
1138
|
+
|
|
1139
|
+
seeds = load_all_seeds(seeds_dir)
|
|
1140
|
+
|
|
1141
|
+
if not seeds:
|
|
1142
|
+
click.echo("No seed files found in directory.", err=True)
|
|
1143
|
+
raise SystemExit(1)
|
|
1144
|
+
|
|
1145
|
+
# Filter personas
|
|
1146
|
+
if persona:
|
|
1147
|
+
seeds = {k: v for k, v in seeds.items() if k in persona}
|
|
1148
|
+
if not seeds:
|
|
1149
|
+
click.echo(f"No matching personas: {', '.join(persona)}", err=True)
|
|
1150
|
+
raise SystemExit(1)
|
|
1151
|
+
|
|
1152
|
+
# Build the prompt
|
|
1153
|
+
target_path = Path(target)
|
|
1154
|
+
lines = [
|
|
1155
|
+
"# Review Gauntlet Prompt\n",
|
|
1156
|
+
"You are running the Review Gauntlet. Apply these rules ruthlessly.\n",
|
|
1157
|
+
"## Target\n",
|
|
1158
|
+
f"Review: `{target_path}`\n",
|
|
1159
|
+
"## Reviewers and Rules\n",
|
|
1160
|
+
]
|
|
1161
|
+
|
|
1162
|
+
for name, sf in seeds.items():
|
|
1163
|
+
persona_name = name.replace("_", " ").title()
|
|
1164
|
+
lines.append(f"### {persona_name}\n")
|
|
1165
|
+
for r in sf.rules:
|
|
1166
|
+
lines.append(f"- **{r.rule}**")
|
|
1167
|
+
if r.antipattern:
|
|
1168
|
+
lines.append(f" - Antipattern: {r.antipattern}")
|
|
1169
|
+
lines.append("")
|
|
1170
|
+
|
|
1171
|
+
lines.extend(
|
|
1172
|
+
[
|
|
1173
|
+
"## Output Format\n",
|
|
1174
|
+
"For each issue found, output:\n",
|
|
1175
|
+
"```json",
|
|
1176
|
+
"{",
|
|
1177
|
+
' "reviewer": "<persona>",',
|
|
1178
|
+
' "severity": "critical|major|minor|nitpick",',
|
|
1179
|
+
' "category": "<category>",',
|
|
1180
|
+
' "location": "<file:line>",',
|
|
1181
|
+
' "description": "<what is wrong>",',
|
|
1182
|
+
' "rule_learned": "<generalizable rule>"',
|
|
1183
|
+
"}",
|
|
1184
|
+
"```\n",
|
|
1185
|
+
"## Instructions\n",
|
|
1186
|
+
"1. Read the target code thoroughly",
|
|
1187
|
+
"2. Apply each rule from each reviewer",
|
|
1188
|
+
"3. Report ALL violations found",
|
|
1189
|
+
"4. Be ruthless - this is the gauntlet",
|
|
1190
|
+
"",
|
|
1191
|
+
]
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
formatted = "\n".join(lines)
|
|
1195
|
+
|
|
1196
|
+
if output:
|
|
1197
|
+
output_path = Path(output)
|
|
1198
|
+
output_path.write_text(formatted, encoding="utf-8")
|
|
1199
|
+
click.echo(f"Wrote prompt to {output_path}")
|
|
1200
|
+
else:
|
|
1201
|
+
click.echo(formatted)
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
@gauntlet.command("learn")
|
|
1205
|
+
@click.argument("issues_file", type=click.Path(exists=True))
|
|
1206
|
+
@click.option("--source", "-s", help="Source identifier (e.g., 'gauntlet:PR#42')")
|
|
1207
|
+
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
|
|
1208
|
+
def gauntlet_learn(issues_file: str, source: str | None, output_json: bool):
|
|
1209
|
+
"""Persist learnings from a gauntlet review.
|
|
1210
|
+
|
|
1211
|
+
Takes a JSON file of issues (in the gauntlet output format)
|
|
1212
|
+
and calls learn_from_review to persist them.
|
|
1213
|
+
|
|
1214
|
+
Examples:
|
|
1215
|
+
|
|
1216
|
+
buildlog gauntlet learn review_issues.json
|
|
1217
|
+
buildlog gauntlet learn issues.json --source "gauntlet:2026-01-22"
|
|
1218
|
+
"""
|
|
1219
|
+
import json as json_module
|
|
1220
|
+
from dataclasses import asdict
|
|
1221
|
+
|
|
1222
|
+
from buildlog.core import learn_from_review
|
|
1223
|
+
|
|
1224
|
+
buildlog_dir = Path("buildlog")
|
|
1225
|
+
|
|
1226
|
+
if not buildlog_dir.exists():
|
|
1227
|
+
click.echo("No buildlog/ directory found. Run 'buildlog init' first.", err=True)
|
|
1228
|
+
raise SystemExit(1)
|
|
1229
|
+
|
|
1230
|
+
# Load issues
|
|
1231
|
+
try:
|
|
1232
|
+
with open(issues_file) as f:
|
|
1233
|
+
data = json_module.load(f)
|
|
1234
|
+
except json_module.JSONDecodeError as e:
|
|
1235
|
+
click.echo(f"Invalid JSON: {e}", err=True)
|
|
1236
|
+
raise SystemExit(1)
|
|
1237
|
+
|
|
1238
|
+
# Handle different formats
|
|
1239
|
+
if isinstance(data, list):
|
|
1240
|
+
issues = data
|
|
1241
|
+
elif isinstance(data, dict) and "all_issues" in data:
|
|
1242
|
+
issues = data["all_issues"]
|
|
1243
|
+
elif isinstance(data, dict) and "issues" in data:
|
|
1244
|
+
issues = data["issues"]
|
|
1245
|
+
else:
|
|
1246
|
+
click.echo(
|
|
1247
|
+
"Expected list of issues or dict with 'issues'/'all_issues'", err=True
|
|
1248
|
+
)
|
|
1249
|
+
raise SystemExit(1)
|
|
1250
|
+
|
|
1251
|
+
if not issues:
|
|
1252
|
+
click.echo("No issues found in file.", err=True)
|
|
1253
|
+
raise SystemExit(1)
|
|
1254
|
+
|
|
1255
|
+
# Learn from review
|
|
1256
|
+
result = learn_from_review(buildlog_dir, issues, source=source or "gauntlet")
|
|
1257
|
+
|
|
1258
|
+
if output_json:
|
|
1259
|
+
click.echo(json_module.dumps(asdict(result), indent=2))
|
|
1260
|
+
else:
|
|
1261
|
+
click.echo(f"✓ {result.message}")
|
|
1262
|
+
click.echo(f" New learnings: {result.new_learnings}")
|
|
1263
|
+
click.echo(f" Reinforced: {result.reinforced_learnings}")
|
|
1264
|
+
click.echo(f" Total processed: {result.total_issues_processed}")
|
|
1265
|
+
|
|
1266
|
+
|
|
879
1267
|
if __name__ == "__main__":
|
|
880
1268
|
main()
|
|
File without changes
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Security Karen - Curated Security Rules
|
|
2
|
+
# Source: OWASP Top 10 (2021), CWE, security best practices
|
|
3
|
+
# These rules are defensible - each links to authoritative sources
|
|
4
|
+
|
|
5
|
+
persona: security_karen
|
|
6
|
+
version: 1
|
|
7
|
+
|
|
8
|
+
rules:
|
|
9
|
+
# A01:2021 - Broken Access Control
|
|
10
|
+
- rule: "Verify authorization on every privileged operation"
|
|
11
|
+
category: security
|
|
12
|
+
context: "Any endpoint or function that modifies data or accesses sensitive resources"
|
|
13
|
+
antipattern: "Checking authentication but not authorization; assuming authenticated = authorized"
|
|
14
|
+
rationale: "Broken access control is OWASP #1. Users can act outside intended permissions."
|
|
15
|
+
tags: [access-control, authorization, owasp-a01]
|
|
16
|
+
references:
|
|
17
|
+
- url: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
|
|
18
|
+
title: "OWASP A01:2021 Broken Access Control"
|
|
19
|
+
- url: "https://cwe.mitre.org/data/definitions/862.html"
|
|
20
|
+
title: "CWE-862: Missing Authorization"
|
|
21
|
+
|
|
22
|
+
- rule: "Deny by default for access control decisions"
|
|
23
|
+
category: security
|
|
24
|
+
context: "Access control logic, permission checks, authorization middleware"
|
|
25
|
+
antipattern: "Allowing access unless explicitly denied; missing else clause in auth checks"
|
|
26
|
+
rationale: "Fail-open access control leads to unauthorized access. Fail-closed is safer."
|
|
27
|
+
tags: [access-control, authorization, defense-in-depth]
|
|
28
|
+
references:
|
|
29
|
+
- url: "https://owasp.org/Top10/A01_2021-Broken_Access_Control/"
|
|
30
|
+
title: "OWASP A01:2021 Broken Access Control"
|
|
31
|
+
|
|
32
|
+
# A02:2021 - Cryptographic Failures
|
|
33
|
+
- rule: "Never store passwords in plaintext or reversible encryption"
|
|
34
|
+
category: security
|
|
35
|
+
context: "User registration, password storage, credential management"
|
|
36
|
+
antipattern: "Storing raw passwords; using MD5/SHA1 without salt; using encryption instead of hashing"
|
|
37
|
+
rationale: "Password leaks expose users. Use bcrypt/argon2 with proper work factors."
|
|
38
|
+
tags: [cryptography, passwords, owasp-a02]
|
|
39
|
+
references:
|
|
40
|
+
- url: "https://owasp.org/Top10/A02_2021-Cryptographic_Failures/"
|
|
41
|
+
title: "OWASP A02:2021 Cryptographic Failures"
|
|
42
|
+
- url: "https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html"
|
|
43
|
+
title: "OWASP Password Storage Cheat Sheet"
|
|
44
|
+
|
|
45
|
+
- rule: "Use TLS 1.2+ for all data in transit"
|
|
46
|
+
category: security
|
|
47
|
+
context: "API calls, database connections, service-to-service communication"
|
|
48
|
+
antipattern: "HTTP endpoints; TLS 1.0/1.1; self-signed certs in production without pinning"
|
|
49
|
+
rationale: "Data in transit can be intercepted. TLS provides confidentiality and integrity."
|
|
50
|
+
tags: [cryptography, tls, owasp-a02]
|
|
51
|
+
references:
|
|
52
|
+
- url: "https://owasp.org/Top10/A02_2021-Cryptographic_Failures/"
|
|
53
|
+
title: "OWASP A02:2021 Cryptographic Failures"
|
|
54
|
+
|
|
55
|
+
# A03:2021 - Injection
|
|
56
|
+
- rule: "Parameterize all database queries"
|
|
57
|
+
category: security
|
|
58
|
+
context: "Any code constructing SQL, NoSQL, LDAP, or OS commands from input"
|
|
59
|
+
antipattern: "String concatenation with user input; f-strings in queries; raw SQL with variables"
|
|
60
|
+
rationale: "SQL injection allows data theft, modification, or deletion. Parameterization prevents it."
|
|
61
|
+
tags: [injection, sql, owasp-a03]
|
|
62
|
+
references:
|
|
63
|
+
- url: "https://owasp.org/Top10/A03_2021-Injection/"
|
|
64
|
+
title: "OWASP A03:2021 Injection"
|
|
65
|
+
- url: "https://cwe.mitre.org/data/definitions/89.html"
|
|
66
|
+
title: "CWE-89: SQL Injection"
|
|
67
|
+
|
|
68
|
+
- rule: "Escape or sanitize all output to prevent XSS"
|
|
69
|
+
category: security
|
|
70
|
+
context: "Rendering user-provided content in HTML, JavaScript, or other executable contexts"
|
|
71
|
+
antipattern: "innerHTML with user data; dangerouslySetInnerHTML; template literals without escaping"
|
|
72
|
+
rationale: "XSS allows attackers to execute code in users' browsers. Always escape output."
|
|
73
|
+
tags: [injection, xss, owasp-a03]
|
|
74
|
+
references:
|
|
75
|
+
- url: "https://owasp.org/Top10/A03_2021-Injection/"
|
|
76
|
+
title: "OWASP A03:2021 Injection"
|
|
77
|
+
- url: "https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html"
|
|
78
|
+
title: "OWASP XSS Prevention Cheat Sheet"
|
|
79
|
+
|
|
80
|
+
# A04:2021 - Insecure Design
|
|
81
|
+
- rule: "Implement rate limiting on authentication endpoints"
|
|
82
|
+
category: security
|
|
83
|
+
context: "Login, registration, password reset, API authentication"
|
|
84
|
+
antipattern: "Unlimited login attempts; no lockout; no CAPTCHA on repeated failures"
|
|
85
|
+
rationale: "Credential stuffing and brute force attacks are common. Rate limiting mitigates them."
|
|
86
|
+
tags: [authentication, rate-limiting, owasp-a04]
|
|
87
|
+
references:
|
|
88
|
+
- url: "https://owasp.org/Top10/A04_2021-Insecure_Design/"
|
|
89
|
+
title: "OWASP A04:2021 Insecure Design"
|
|
90
|
+
- url: "https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html"
|
|
91
|
+
title: "OWASP Authentication Cheat Sheet"
|
|
92
|
+
|
|
93
|
+
# A05:2021 - Security Misconfiguration
|
|
94
|
+
- rule: "Never commit secrets to version control"
|
|
95
|
+
category: security
|
|
96
|
+
context: "Any code or configuration file that might contain API keys, passwords, tokens"
|
|
97
|
+
antipattern: "Hardcoded credentials; .env files in git; secrets in docker-compose.yml"
|
|
98
|
+
rationale: "Git history is forever. Leaked secrets lead to account compromise."
|
|
99
|
+
tags: [secrets, configuration, owasp-a05]
|
|
100
|
+
references:
|
|
101
|
+
- url: "https://owasp.org/Top10/A05_2021-Security_Misconfiguration/"
|
|
102
|
+
title: "OWASP A05:2021 Security Misconfiguration"
|
|
103
|
+
|
|
104
|
+
- rule: "Disable debug mode and verbose errors in production"
|
|
105
|
+
category: security
|
|
106
|
+
context: "Production deployments, error handling, logging configuration"
|
|
107
|
+
antipattern: "DEBUG=True in production; stack traces in API responses; verbose SQL errors"
|
|
108
|
+
rationale: "Debug info reveals implementation details useful for attackers."
|
|
109
|
+
tags: [configuration, errors, owasp-a05]
|
|
110
|
+
references:
|
|
111
|
+
- url: "https://owasp.org/Top10/A05_2021-Security_Misconfiguration/"
|
|
112
|
+
title: "OWASP A05:2021 Security Misconfiguration"
|
|
113
|
+
|
|
114
|
+
# A07:2021 - Identification and Authentication Failures
|
|
115
|
+
- rule: "Use secure session management with httpOnly and secure flags"
|
|
116
|
+
category: security
|
|
117
|
+
context: "Session cookies, JWT storage, authentication tokens"
|
|
118
|
+
antipattern: "Tokens in localStorage; cookies without httpOnly; missing secure flag"
|
|
119
|
+
rationale: "Insecure session storage enables session hijacking via XSS."
|
|
120
|
+
tags: [authentication, sessions, owasp-a07]
|
|
121
|
+
references:
|
|
122
|
+
- url: "https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
|
|
123
|
+
title: "OWASP A07:2021 Identification and Authentication Failures"
|
|
124
|
+
- url: "https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html"
|
|
125
|
+
title: "OWASP Session Management Cheat Sheet"
|
|
126
|
+
|
|
127
|
+
# A08:2021 - Software and Data Integrity Failures
|
|
128
|
+
- rule: "Verify integrity of external dependencies"
|
|
129
|
+
category: security
|
|
130
|
+
context: "Package installation, CI/CD pipelines, dependency updates"
|
|
131
|
+
antipattern: "No lockfile; no hash verification; installing from arbitrary URLs"
|
|
132
|
+
rationale: "Supply chain attacks inject malicious code via dependencies."
|
|
133
|
+
tags: [dependencies, supply-chain, owasp-a08]
|
|
134
|
+
references:
|
|
135
|
+
- url: "https://owasp.org/Top10/A08_2021-Software_and_Data_Integrity_Failures/"
|
|
136
|
+
title: "OWASP A08:2021 Software and Data Integrity Failures"
|
|
137
|
+
|
|
138
|
+
# A09:2021 - Security Logging and Monitoring Failures
|
|
139
|
+
- rule: "Log security-relevant events with sufficient context"
|
|
140
|
+
category: security
|
|
141
|
+
context: "Authentication, authorization failures, input validation failures"
|
|
142
|
+
antipattern: "No logging; logging without timestamps; no user/IP context; PII in logs"
|
|
143
|
+
rationale: "Without logs, breaches go undetected. Logs enable incident response."
|
|
144
|
+
tags: [logging, monitoring, owasp-a09]
|
|
145
|
+
references:
|
|
146
|
+
- url: "https://owasp.org/Top10/A09_2021-Security_Logging_and_Monitoring_Failures/"
|
|
147
|
+
title: "OWASP A09:2021 Security Logging and Monitoring Failures"
|
|
148
|
+
- url: "https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html"
|
|
149
|
+
title: "OWASP Logging Cheat Sheet"
|
|
150
|
+
|
|
151
|
+
# A10:2021 - Server-Side Request Forgery (SSRF)
|
|
152
|
+
- rule: "Validate and allowlist URLs for server-side requests"
|
|
153
|
+
category: security
|
|
154
|
+
context: "Fetching external URLs, webhooks, image proxies, URL shorteners"
|
|
155
|
+
antipattern: "Fetching arbitrary user-provided URLs; no protocol/host validation"
|
|
156
|
+
rationale: "SSRF allows attackers to access internal services or cloud metadata."
|
|
157
|
+
tags: [ssrf, input-validation, owasp-a10]
|
|
158
|
+
references:
|
|
159
|
+
- url: "https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/"
|
|
160
|
+
title: "OWASP A10:2021 SSRF"
|
|
161
|
+
- url: "https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html"
|
|
162
|
+
title: "OWASP SSRF Prevention Cheat Sheet"
|