Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@ __pycache__/
*.pyc
.DS_Store
Thumbs.db

# Personal lore store (`personalStoreDir` in .lore.json) — per-person notes about
# developing this plugin, read by recall alongside the team store but never
# published. `learnings/` is deliberately NOT ignored here: the team store is
# committed and shared by default, and that default is what users of the plugin
# get.
.lore/
3 changes: 3 additions & 0 deletions .lore.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"personalStoreDir": ".lore/personal"
}
14 changes: 11 additions & 3 deletions plugin/scripts/scan_secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,18 @@
("JWT", r"\beyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]{10,}\b"),
("Bearer token", r"(?i)\bbearer\s+[A-Za-z0-9_\-\.=]{20,}"),
("Credentials in URL", r"\b[a-z][a-z0-9+.\-]*://[^\s:@/]+:([^\s:@/]{3,})@[^\s/]+"),
# secret-ish assignment with a real-looking value (placeholders filtered below)
# secret-ish assignment with a real-looking value (placeholders filtered below).
# The keyword is deliberately NOT wrapped in `\b`: `_` is a word character, so
# `\b` never fires between an underscore and a letter, and `db_password`,
# `STRIPE_SECRET` and `aws_secret_access_key` — the dominant real-world
# spellings — all slipped through. Affixes are flat character classes rather
# than nested quantifiers, so backtracking stays linear.
# The value must stay capture group 1: `_is_placeholder` reads `group(1)`.
("Hardcoded secret assignment",
r"(?i)\b(?:password|passwd|pwd|secret|api[_-]?key|access[_-]?token|"
r"auth[_-]?token|client[_-]?secret|private[_-]?key)\b\s*[:=]\s*"
r"(?i)(?:^|[^A-Za-z0-9])[A-Za-z0-9_.-]{0,40}"
r"(?:password|passwd|pwd|secret|api[_-]?key|access[_-]?token|"
r"auth[_-]?token|client[_-]?secret|private[_-]?key)"
r"[A-Za-z0-9_.-]{0,40}\s*[:=]\s*"
r"['\"]?([^\s'\"]{6,})['\"]?"),
]
RULES = [(name, re.compile(pat)) for name, pat in _RULES]
Expand Down
20 changes: 20 additions & 0 deletions tests/test_scan_secrets.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ def test_real_looking_assignment_flagged(self):
found = _scan('password = "h8s7d6f5g4h3j2k1"')
self.assertEqual(len(found), 1)

def test_prefixed_secret_names_flagged(self):
# `\b` cannot fire between `_` and a letter, so an earlier version of this
# rule missed every prefixed spelling -- which is most of them in practice.
for line in ('db_password = "h8s7d6f5g4h3j2k1"',
'my_api_key = "AbCd1234EfGh5678"',
'STRIPE_SECRET = "AbCd1234EfGh5678"',
'aws_secret_access_key = "wJalrXUtnFEMI7K7MDENGbPxRfiCYEX"',
"DATABASE_PASSWORD=s3cr3tp4ssw0rd"):
with self.subTest(line=line):
self.assertEqual(len(_scan(line)), 1)

def test_prefixed_placeholder_still_not_flagged(self):
# Widening the name must not cost the placeholder filter its job.
self.assertEqual(_scan("db_password = changeme"), [])
self.assertEqual(_scan('my_api_key = "your_key_here"'), [])

def test_short_or_nonsecret_values_not_flagged(self):
self.assertEqual(_scan("password: x"), [])
self.assertEqual(_scan("secret_name = db"), [])

def test_inline_allow(self):
fake = "AKIA" + "C" * 16
self.assertEqual(_scan(f"{fake} lore:allow-secret"), [])
Expand Down
Loading