security: support private identifier deny lists
This commit is contained in:
parent
16c1b5f192
commit
bd53ee640f
3 changed files with 43 additions and 3 deletions
|
|
@ -22,6 +22,9 @@ project artifact is created, edited, renamed, or reorganized there.
|
||||||
- Never commit LightBurn Support Data to the public repository.
|
- Never commit LightBurn Support Data to the public repository.
|
||||||
- Do not execute software acquired from the installer stick during analysis.
|
- Do not execute software acquired from the installer stick during analysis.
|
||||||
- Run the public repository guard before each public push.
|
- Run the public repository guard before each public push.
|
||||||
|
- When PrivateStash is available, pass its private deny-list to the public
|
||||||
|
guard so known serials, source IDs, and local identifiers are checked without
|
||||||
|
storing those values publicly.
|
||||||
|
|
||||||
## Redaction flow
|
## Redaction flow
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ def tracked_files(root: Path) -> list[Path]:
|
||||||
return [root / item.decode("utf-8") for item in result.stdout.split(b"\0") if item]
|
return [root / item.decode("utf-8") for item in result.stdout.split(b"\0") if item]
|
||||||
|
|
||||||
|
|
||||||
def check(root: Path) -> list[str]:
|
def check(root: Path, deny_patterns: tuple[str, ...] = ()) -> list[str]:
|
||||||
failures: list[str] = []
|
failures: list[str] = []
|
||||||
for path in tracked_files(root):
|
for path in tracked_files(root):
|
||||||
relative = path.relative_to(root).as_posix()
|
relative = path.relative_to(root).as_posix()
|
||||||
|
|
@ -48,14 +48,36 @@ def check(root: Path) -> list[str]:
|
||||||
for label, pattern in SENSITIVE_TEXT.items():
|
for label, pattern in SENSITIVE_TEXT.items():
|
||||||
if pattern.search(content):
|
if pattern.search(content):
|
||||||
failures.append(f"{label} found in {relative}")
|
failures.append(f"{label} found in {relative}")
|
||||||
|
folded = content.casefold()
|
||||||
|
for pattern in deny_patterns:
|
||||||
|
if pattern.casefold() in folded:
|
||||||
|
failures.append(f"private deny-list token found in {relative}")
|
||||||
return failures
|
return failures
|
||||||
|
|
||||||
|
|
||||||
|
def load_deny_patterns(path: Path | None) -> tuple[str, ...]:
|
||||||
|
if path is None:
|
||||||
|
return ()
|
||||||
|
patterns = []
|
||||||
|
for line in path.read_text(encoding="utf-8").splitlines():
|
||||||
|
candidate = line.strip()
|
||||||
|
if candidate and not candidate.startswith("#"):
|
||||||
|
patterns.append(candidate)
|
||||||
|
return tuple(patterns)
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("root", type=Path, nargs="?", default=Path.cwd())
|
parser.add_argument("root", type=Path, nargs="?", default=Path.cwd())
|
||||||
|
parser.add_argument(
|
||||||
|
"--deny-pattern-file",
|
||||||
|
type=Path,
|
||||||
|
help="private newline-delimited literal tokens that must not appear publicly",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
failures = check(args.root.resolve())
|
failures = check(
|
||||||
|
args.root.resolve(), load_deny_patterns(args.deny_pattern_file)
|
||||||
|
)
|
||||||
if failures:
|
if failures:
|
||||||
for failure in failures:
|
for failure in failures:
|
||||||
print(f"ERROR: {failure}")
|
print(f"ERROR: {failure}")
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from scripts.check_public_repo import check
|
from scripts.check_public_repo import check, load_deny_patterns
|
||||||
|
|
||||||
|
|
||||||
class PublicRepoGuardTests(unittest.TestCase):
|
class PublicRepoGuardTests(unittest.TestCase):
|
||||||
|
|
@ -36,6 +36,21 @@ class PublicRepoGuardTests(unittest.TestCase):
|
||||||
with patch("scripts.check_public_repo.tracked_files", return_value=[candidate]):
|
with patch("scripts.check_public_repo.tracked_files", return_value=[candidate]):
|
||||||
self.assertEqual(check(root), [])
|
self.assertEqual(check(root), [])
|
||||||
|
|
||||||
|
def test_private_deny_list_is_applied_without_echoing_token(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
candidate = root / "notes.md"
|
||||||
|
candidate.write_text("machine-sensitive-value", encoding="utf-8")
|
||||||
|
with patch("scripts.check_public_repo.tracked_files", return_value=[candidate]):
|
||||||
|
failures = check(root, ("sensitive-value",))
|
||||||
|
self.assertEqual(failures, ["private deny-list token found in notes.md"])
|
||||||
|
|
||||||
|
def test_deny_list_ignores_comments_and_blank_lines(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
path = Path(temporary) / "deny.txt"
|
||||||
|
path.write_text("# private values\n\n first-token \nsecond-token\n", encoding="utf-8")
|
||||||
|
self.assertEqual(load_deny_patterns(path), ("first-token", "second-token"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue