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.
Files changed (26) hide show
  1. buildlog/cli.py +391 -3
  2. buildlog/data/__init__.py +0 -0
  3. buildlog/data/seeds/security_karen.yaml +162 -0
  4. buildlog/data/seeds/test_terrorist.yaml +280 -0
  5. buildlog/seed_engine/__init__.py +74 -0
  6. buildlog/seed_engine/categorizers.py +145 -0
  7. buildlog/seed_engine/extractors.py +148 -0
  8. buildlog/seed_engine/generators.py +144 -0
  9. buildlog/seed_engine/models.py +113 -0
  10. buildlog/seed_engine/pipeline.py +202 -0
  11. buildlog/seed_engine/sources.py +362 -0
  12. buildlog/seeds.py +261 -0
  13. buildlog/skills.py +26 -3
  14. {buildlog-0.5.0.dist-info → buildlog-0.6.1.dist-info}/METADATA +82 -11
  15. buildlog-0.6.1.dist-info/RECORD +41 -0
  16. buildlog-0.5.0.dist-info/RECORD +0 -30
  17. {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/copier.yml +0 -0
  18. {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/post_gen.py +0 -0
  19. {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/template/buildlog/.gitkeep +0 -0
  20. {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/template/buildlog/2026-01-01-example.md +0 -0
  21. {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/template/buildlog/BUILDLOG_SYSTEM.md +0 -0
  22. {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/template/buildlog/_TEMPLATE.md +0 -0
  23. {buildlog-0.5.0.data → buildlog-0.6.1.data}/data/share/buildlog/template/buildlog/assets/.gitkeep +0 -0
  24. {buildlog-0.5.0.dist-info → buildlog-0.6.1.dist-info}/WHEEL +0 -0
  25. {buildlog-0.5.0.dist-info → buildlog-0.6.1.dist-info}/entry_points.txt +0 -0
  26. {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 list():
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"), reverse=True # Most recent first
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"