gitinject 0.1.0__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 (164) hide show
  1. gitinject/__init__.py +1 -0
  2. gitinject/__main__.py +5 -0
  3. gitinject/analyzer.py +67 -0
  4. gitinject/attacks/__init__.py +27 -0
  5. gitinject/attacks/autoinject.py +165 -0
  6. gitinject/attacks/base.py +33 -0
  7. gitinject/attacks/static.py +25 -0
  8. gitinject/cli.py +768 -0
  9. gitinject/data/research/scenarios/claude_skills_injection.md +96 -0
  10. gitinject/data/research/scenarios/cline_issue_body_injection.md +93 -0
  11. gitinject/data/research/scenarios/codex_agents_md_injection.md +109 -0
  12. gitinject/data/research/scenarios/dos_request_flood.md +133 -0
  13. gitinject/data/research/scenarios/dropped/ci_log_injection_workflow_poisoning.md +138 -0
  14. gitinject/data/research/scenarios/dropped/claude_md_instructions_injection.md +158 -0
  15. gitinject/data/research/scenarios/dropped/supply_chain_token_pivot.md +101 -0
  16. gitinject/data/research/scenarios/gemini_api_key_exfiltration.md +68 -0
  17. gitinject/data/research/scenarios/gemini_api_key_exfiltration_replication.md +0 -0
  18. gitinject/data/research/scenarios/gemini_md_instructions_injection.md +131 -0
  19. gitinject/data/research/scenarios/nsfw_api_key_block.md +114 -0
  20. gitinject/data/research/scenarios/pr_token_exfiltration_via_git_config.md +117 -0
  21. gitinject/data/research/scenarios/supply_chain_malicious_code.md +135 -0
  22. gitinject/evaluators.py +169 -0
  23. gitinject/evidence.py +72 -0
  24. gitinject/gl_runner.py +163 -0
  25. gitinject/resources.py +30 -0
  26. gitinject/run_context.py +72 -0
  27. gitinject/run_record.py +113 -0
  28. gitinject/runner.py +1024 -0
  29. gitinject/scanner/__init__.py +22 -0
  30. gitinject/scanner/baselines/__init__.py +0 -0
  31. gitinject/scanner/baselines/actionlint_runner.py +44 -0
  32. gitinject/scanner/baselines/zizmor_runner.py +35 -0
  33. gitinject/scanner/diagnostics.py +174 -0
  34. gitinject/scanner/hypothesis_generator.py +164 -0
  35. gitinject/scanner/live_validator.py +284 -0
  36. gitinject/scanner/llm_ranker.py +120 -0
  37. gitinject/scanner/memory.py +239 -0
  38. gitinject/scanner/primitives.py +236 -0
  39. gitinject/scanner/prompt_extractor.py +134 -0
  40. gitinject/scanner/recipe_scenario.py +203 -0
  41. gitinject/scanner/report_generator.py +230 -0
  42. gitinject/scanner/types.py +179 -0
  43. gitinject/scenario_base.py +147 -0
  44. gitinject/scenario_loader.py +77 -0
  45. gitinject/scenarios/__init__.py +0 -0
  46. gitinject/scenarios/benign/documentation_out_of_sync/contents/math_utils.py +3 -0
  47. gitinject/scenarios/benign/documentation_out_of_sync/contents/pydantic/v1/utils.py +21 -0
  48. gitinject/scenarios/benign/documentation_out_of_sync/scenario.py +57 -0
  49. gitinject/scenarios/benign/issue_deduplication_utility/scenario.py +76 -0
  50. gitinject/scenarios/benign/repository_onboarding_qa/contents/README.md +3 -0
  51. gitinject/scenarios/benign/repository_onboarding_qa/scenario.py +52 -0
  52. gitinject/scenarios/benign/secrets_false_positive/contents/tests/test_auth.py +14 -0
  53. gitinject/scenarios/benign/secrets_false_positive/scenario.py +58 -0
  54. gitinject/scenarios/benign/test_coverage_gap/contents/src/requests/auth_extra.py +33 -0
  55. gitinject/scenarios/benign/test_coverage_gap/scenario.py +56 -0
  56. gitinject/scenarios/benign/vulnerability_fix_verification/contents/src/flask/utils.py +15 -0
  57. gitinject/scenarios/benign/vulnerability_fix_verification/scenario.py +57 -0
  58. gitinject/scenarios/benign/vulnerable_code_review/contents/examples/tutorial/flaskr/db_utils.py +18 -0
  59. gitinject/scenarios/benign/vulnerable_code_review/scenario.py +55 -0
  60. gitinject/scenarios/benign/weekly_activity_summary/scenario.py +83 -0
  61. gitinject/scenarios/malicious/agents_md_approval_manipulation/contents/AGENTS.md +25 -0
  62. gitinject/scenarios/malicious/agents_md_approval_manipulation/contents/src/flask/csrf.py +151 -0
  63. gitinject/scenarios/malicious/agents_md_approval_manipulation/contents/tests/test_csrf.py +71 -0
  64. gitinject/scenarios/malicious/agents_md_approval_manipulation/scenario.py +121 -0
  65. gitinject/scenarios/malicious/claude_md_instructions_injection/contents/CLAUDE.md +23 -0
  66. gitinject/scenarios/malicious/claude_md_instructions_injection/contents/scripts/deploy_check.sh +29 -0
  67. gitinject/scenarios/malicious/claude_md_instructions_injection/scenario.py +93 -0
  68. gitinject/scenarios/malicious/cline_issue_body_injection/scenario.py +132 -0
  69. gitinject/scenarios/malicious/codex_agents_md_injection/contents/AGENTS.md +36 -0
  70. gitinject/scenarios/malicious/codex_agents_md_injection/contents/scripts/utils.py +12 -0
  71. gitinject/scenarios/malicious/codex_agents_md_injection/scenario.py +128 -0
  72. gitinject/scenarios/malicious/dos_request_flood/scenario.py +80 -0
  73. gitinject/scenarios/malicious/gemini_md_approval_manipulation/contents/src/flask/csrf.py +151 -0
  74. gitinject/scenarios/malicious/gemini_md_approval_manipulation/contents/tests/test_csrf.py +71 -0
  75. gitinject/scenarios/malicious/gemini_md_approval_manipulation/scenario.py +121 -0
  76. gitinject/scenarios/malicious/gemini_md_instructions_injection/contents/scripts/deploy_check.sh +38 -0
  77. gitinject/scenarios/malicious/gemini_md_instructions_injection/scenario.py +105 -0
  78. gitinject/scenarios/malicious/issue_onboarding_secret_exfiltration/scenario.py +76 -0
  79. gitinject/scenarios/malicious/pr_token_exfiltration_via_git_config/contents/scripts/deploy_check.sh +7 -0
  80. gitinject/scenarios/malicious/pr_token_exfiltration_via_git_config/scenario.py +113 -0
  81. gitinject/scenarios/malicious/supply_chain_malicious_code/contents/CLAUDE.md +25 -0
  82. gitinject/scenarios/malicious/supply_chain_malicious_code/contents/src/flask/csrf.py +151 -0
  83. gitinject/scenarios/malicious/supply_chain_malicious_code/contents/tests/test_csrf.py +71 -0
  84. gitinject/scenarios/malicious/supply_chain_malicious_code/scenario.py +123 -0
  85. gitinject/scenarios/malicious/unauthorized_pr_approval/contents/django/utils/crypto.py +13 -0
  86. gitinject/scenarios/malicious/unauthorized_pr_approval/scenario.py +57 -0
  87. gitinject/simulator.py +89 -0
  88. gitinject/utils/__init__.py +0 -0
  89. gitinject/utils/gh_client.py +628 -0
  90. gitinject/utils/gl_client.py +132 -0
  91. gitinject/utils/gl_provisioner.py +83 -0
  92. gitinject/utils/llm.py +205 -0
  93. gitinject/utils/provisioner.py +114 -0
  94. gitinject/utils/scenario_resources.py +33 -0
  95. gitinject/utils/types.py +49 -0
  96. gitinject/workflows/__init__.py +0 -0
  97. gitinject/workflows/claude-ci-auto-fix/contents/.github/workflows/main.yml +107 -0
  98. gitinject/workflows/claude-ci-auto-fix/metadata.json +10 -0
  99. gitinject/workflows/claude-general/contents/.github/workflows/main.yml +58 -0
  100. gitinject/workflows/claude-general/metadata.json +10 -0
  101. gitinject/workflows/claude-gitlab-mr-review/contents/.gitlab-ci.yml +36 -0
  102. gitinject/workflows/claude-gitlab-mr-review/metadata.json +11 -0
  103. gitinject/workflows/claude-issue-deduplication/contents/.github/workflows/main.yml +66 -0
  104. gitinject/workflows/claude-issue-deduplication/metadata.json +10 -0
  105. gitinject/workflows/claude-issue-triage/contents/.github/workflows/main.yml +34 -0
  106. gitinject/workflows/claude-issue-triage/metadata.json +10 -0
  107. gitinject/workflows/claude-manual-analysis/contents/.github/workflows/main.yml +42 -0
  108. gitinject/workflows/claude-manual-analysis/metadata.json +10 -0
  109. gitinject/workflows/claude-pr-review/contents/.github/workflows/main.yml +77 -0
  110. gitinject/workflows/claude-pr-review/metadata.json +10 -0
  111. gitinject/workflows/claude-pr-review-authors/contents/.github/workflows/main.yml +48 -0
  112. gitinject/workflows/claude-pr-review-authors/metadata.json +10 -0
  113. gitinject/workflows/claude-pr-review-paths/contents/.github/workflows/main.yml +49 -0
  114. gitinject/workflows/claude-pr-review-paths/metadata.json +10 -0
  115. gitinject/workflows/claude-test-analysis/contents/.github/workflows/main.yml +114 -0
  116. gitinject/workflows/claude-test-analysis/metadata.json +10 -0
  117. gitinject/workflows/cline-assistant/contents/.github/workflows/main.yml +87 -0
  118. gitinject/workflows/cline-assistant/contents/git-scripts/analyze-issue.sh +43 -0
  119. gitinject/workflows/cline-assistant/metadata.json +10 -0
  120. gitinject/workflows/codex-pr-review/contents/.github/workflows/main.yml +73 -0
  121. gitinject/workflows/codex-pr-review/metadata.json +10 -0
  122. gitinject/workflows/copilot-ci-doctor/contents/.github/workflows/ci-doctor.yml +1161 -0
  123. gitinject/workflows/copilot-ci-doctor/metadata.json +10 -0
  124. gitinject/workflows/copilot-lean-squad/contents/.github/workflows/lean-squad.yml +1313 -0
  125. gitinject/workflows/copilot-lean-squad/metadata.json +10 -0
  126. gitinject/workflows/copilot-malicious-scan/contents/.github/workflows/daily-malicious-code-scan.yml +899 -0
  127. gitinject/workflows/copilot-malicious-scan/metadata.json +10 -0
  128. gitinject/workflows/copilot-repo-assist/contents/.github/workflows/repo-assist.yml +1503 -0
  129. gitinject/workflows/copilot-repo-assist/metadata.json +10 -0
  130. gitinject/workflows/copilot-wiki-writer/contents/.github/workflows/agentic-wiki-writer.yml +1316 -0
  131. gitinject/workflows/copilot-wiki-writer/metadata.json +10 -0
  132. gitinject/workflows/gemini-assistant/contents/.github/workflows/gemini-invoke.yml +122 -0
  133. gitinject/workflows/gemini-assistant/contents/.github/workflows/gemini-plan-execute.yml +130 -0
  134. gitinject/workflows/gemini-assistant/contents/.github/workflows/gemini-review.yml +118 -0
  135. gitinject/workflows/gemini-assistant/contents/.github/workflows/gemini-scheduled-triage.yml +220 -0
  136. gitinject/workflows/gemini-assistant/contents/.github/workflows/gemini-triage.yml +160 -0
  137. gitinject/workflows/gemini-assistant/contents/.github/workflows/main.yml +220 -0
  138. gitinject/workflows/gemini-assistant/metadata.json +10 -0
  139. gitinject/workflows/gemini-assistant-original/AWESOME.md +118 -0
  140. gitinject/workflows/gemini-assistant-original/CONFIGURATION.md +162 -0
  141. gitinject/workflows/gemini-assistant-original/README.md +93 -0
  142. gitinject/workflows/gemini-assistant-original/gemini-assistant/README.md +192 -0
  143. gitinject/workflows/gemini-assistant-original/gemini-assistant/gemini-invoke.toml +94 -0
  144. gitinject/workflows/gemini-assistant-original/gemini-assistant/gemini-invoke.yml +131 -0
  145. gitinject/workflows/gemini-assistant-original/gemini-assistant/gemini-plan-execute.toml +100 -0
  146. gitinject/workflows/gemini-assistant-original/gemini-assistant/gemini-plan-execute.yml +139 -0
  147. gitinject/workflows/gemini-assistant-original/gemini-dispatch/README.md +49 -0
  148. gitinject/workflows/gemini-assistant-original/gemini-dispatch/gemini-dispatch.yml +221 -0
  149. gitinject/workflows/gemini-assistant-original/issue-triage/README.md +190 -0
  150. gitinject/workflows/gemini-assistant-original/issue-triage/gemini-scheduled-triage.toml +96 -0
  151. gitinject/workflows/gemini-assistant-original/issue-triage/gemini-scheduled-triage.yml +223 -0
  152. gitinject/workflows/gemini-assistant-original/issue-triage/gemini-triage.toml +32 -0
  153. gitinject/workflows/gemini-assistant-original/issue-triage/gemini-triage.yml +167 -0
  154. gitinject/workflows/gemini-assistant-original/metadata.json +10 -0
  155. gitinject/workflows/gemini-assistant-original/pr-review/README.md +337 -0
  156. gitinject/workflows/gemini-assistant-original/pr-review/gemini-review.toml +176 -0
  157. gitinject/workflows/gemini-assistant-original/pr-review/gemini-review.yml +119 -0
  158. gitinject/workflows/opencode-pr-review/contents/.github/workflows/main.yml +28 -0
  159. gitinject/workflows/opencode-pr-review/metadata.json +10 -0
  160. gitinject-0.1.0.dist-info/METADATA +128 -0
  161. gitinject-0.1.0.dist-info/RECORD +164 -0
  162. gitinject-0.1.0.dist-info/WHEEL +4 -0
  163. gitinject-0.1.0.dist-info/entry_points.txt +2 -0
  164. gitinject-0.1.0.dist-info/licenses/LICENSE +202 -0
gitinject/resources.py ADDED
@@ -0,0 +1,30 @@
1
+ """Locate read-only datasets separately from the writable experiment workspace."""
2
+
3
+ from pathlib import Path
4
+
5
+ PACKAGE_DIR = Path(__file__).resolve().parent
6
+
7
+
8
+ def dataset_dir(name: str, workspace_dir: str | Path | None = None) -> Path:
9
+ """Use a workspace dataset when present, otherwise use the bundled dataset."""
10
+ if name not in {"workflows", "scenarios"}:
11
+ raise ValueError(f"Unknown dataset: {name}")
12
+ workspace = Path(workspace_dir) if workspace_dir is not None else Path.cwd()
13
+ for candidate in (workspace / name, workspace / "src/gitinject" / name, workspace / "src/benchmark" / name):
14
+ if candidate.is_dir():
15
+ return candidate.resolve()
16
+ return PACKAGE_DIR / name
17
+
18
+
19
+ def research_dir(workspace_dir: str | Path | None = None) -> Path:
20
+ """Locate the scanner warm-start notes in a workspace, wheel, or source tree."""
21
+ workspace = Path(workspace_dir) if workspace_dir is not None else Path.cwd()
22
+ candidates = (
23
+ workspace / "research/scenarios",
24
+ PACKAGE_DIR / "data/research/scenarios",
25
+ PACKAGE_DIR.parent.parent / "research/scenarios",
26
+ )
27
+ for candidate in candidates:
28
+ if candidate.is_dir():
29
+ return candidate.resolve()
30
+ raise FileNotFoundError("Scanner warm-start research notes are unavailable")
@@ -0,0 +1,72 @@
1
+ """The runtime capabilities supplied to Python scenarios."""
2
+
3
+ import random
4
+ from dataclasses import dataclass, field
5
+ from typing import Callable, Literal
6
+
7
+ from .run_record import RunRecord, RunSpec
8
+ from .utils.gh_client import GitHubClient
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class TriggerReceipt:
13
+ event_type: str | None = None
14
+ subject_kind: Literal["pr", "issue"] | None = None
15
+ subject_number: int | None = None
16
+ workflow_run_id: int | None = None
17
+
18
+ def __post_init__(self):
19
+ if self.subject_kind not in {None, "pr", "issue"}:
20
+ raise ValueError("Subject kind must be pr or issue")
21
+ if (self.subject_kind is None) != (self.subject_number is None):
22
+ raise ValueError("Subject kind and number must be supplied together")
23
+ for value in (self.subject_number, self.workflow_run_id):
24
+ if value is not None and (type(value) is not int or value <= 0):
25
+ raise ValueError("GitHub identifiers must be positive integers")
26
+
27
+
28
+ @dataclass
29
+ class RunContext:
30
+ spec: RunSpec
31
+ record: RunRecord
32
+ state: dict
33
+ actors: dict[str, GitHubClient]
34
+ default_trigger: Callable[[], TriggerReceipt]
35
+ collect_target: Callable[[], dict]
36
+ _repositories: list[tuple[str, str, int]] = field(default_factory=list)
37
+
38
+ def __post_init__(self):
39
+ self.rng = random.Random(self.spec.seed)
40
+
41
+ @property
42
+ def parameters(self) -> dict:
43
+ return self.spec.parameters
44
+
45
+ def github(self, actor: str) -> GitHubClient:
46
+ if actor not in self.actors:
47
+ raise ValueError(f"Required GitHub actor is unavailable: {actor}")
48
+ return self.actors[actor]
49
+
50
+ def save_artifact(self, name: str, value):
51
+ return self.record.artifact(name, value)
52
+
53
+ def track_repository(self, actor: str, name: str, repository_id: int) -> None:
54
+ """Register a repository immediately after creating it through the raw API."""
55
+ self.github(actor)
56
+ if not name or type(repository_id) is not int or repository_id <= 0:
57
+ raise ValueError("Repository ownership requires a name and immutable ID")
58
+ identity = (actor, name, repository_id)
59
+ if identity not in self._repositories:
60
+ self._repositories.append(identity)
61
+ self.record.event("resource", actor=actor, name=name, id=repository_id, state="created")
62
+
63
+ def cleanup_repositories(self) -> list[str]:
64
+ errors = []
65
+ for actor, name, repository_id in self._repositories[:]:
66
+ try:
67
+ self.github(actor)._delete_repository(name, repository_id)
68
+ self.record.event("resource", actor=actor, name=name, id=repository_id, state="deleted")
69
+ self._repositories.remove((actor, name, repository_id))
70
+ except Exception as exc:
71
+ errors.append(str(exc))
72
+ return errors
@@ -0,0 +1,113 @@
1
+ """Local attempt manifests and append-only execution journals."""
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import uuid
8
+ from dataclasses import asdict, dataclass, field
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class RunSpec:
15
+ workflow: str
16
+ scenario: str
17
+ parameters: dict = field(default_factory=dict)
18
+ seed: int | None = None
19
+ parent_attempt_id: str | None = None
20
+ attack: str | None = None
21
+ cleanup: bool = True
22
+
23
+ def __post_init__(self):
24
+ if not isinstance(self.parameters, dict):
25
+ raise ValueError("Scenario parameters must be a JSON object")
26
+ if self.seed is not None and type(self.seed) is not int:
27
+ raise ValueError("Scenario seed must be an integer")
28
+
29
+
30
+ def write_json(path: Path, value) -> None:
31
+ temporary = path.with_name(path.name + ".tmp")
32
+ with temporary.open("w") as handle:
33
+ json.dump(value, handle, indent=2, allow_nan=False)
34
+ handle.flush()
35
+ os.fsync(handle.fileno())
36
+ temporary.replace(path)
37
+
38
+
39
+ class RunRecord:
40
+ def __init__(self, workspace_dir: str, spec: RunSpec):
41
+ self.attempt_id = uuid.uuid4().hex
42
+ self.timestamp = datetime.now(timezone.utc).isoformat()
43
+ self.directory = Path(workspace_dir) / "runs" / self.attempt_id
44
+ self.directory.mkdir(parents=True)
45
+ self.manifest = {
46
+ "schema_version": 1,
47
+ "attempt_id": self.attempt_id,
48
+ "timestamp": self.timestamp,
49
+ "spec": asdict(spec),
50
+ "inputs": {},
51
+ }
52
+ try:
53
+ revision = subprocess.run(
54
+ ["git", "rev-parse", "HEAD"],
55
+ cwd=workspace_dir,
56
+ capture_output=True,
57
+ text=True,
58
+ check=True,
59
+ ).stdout.strip()
60
+ self.manifest["source_revision"] = revision
61
+ status = subprocess.run(
62
+ ["git", "status", "--porcelain"],
63
+ cwd=workspace_dir,
64
+ capture_output=True,
65
+ text=True,
66
+ check=True,
67
+ ).stdout
68
+ self.manifest["source_dirty"] = bool(status.strip())
69
+ except (OSError, subprocess.CalledProcessError):
70
+ self.manifest["source_revision"] = None
71
+ self.save_manifest()
72
+ self.event("phase", phase="created")
73
+
74
+ def save_manifest(self) -> None:
75
+ write_json(self.directory / "manifest.json", self.manifest)
76
+
77
+ def event(self, kind: str, **data) -> None:
78
+ entry = {"timestamp": datetime.now(timezone.utc).isoformat(), "kind": kind, **data}
79
+ with (self.directory / "events.jsonl").open("a") as handle:
80
+ handle.write(json.dumps(entry, allow_nan=False) + "\n")
81
+ handle.flush()
82
+ os.fsync(handle.fileno())
83
+
84
+ def snapshot(self, label: str, source: str | Path, *, prefix: str = "") -> None:
85
+ source = Path(source)
86
+ files = sorted(source.rglob("*")) if source.is_dir() else [source]
87
+ hashes = dict(self.manifest["inputs"].get(label, {}))
88
+ for path in files:
89
+ if not path.is_file() or "__pycache__" in path.parts:
90
+ continue
91
+ relative = path.relative_to(source) if source.is_dir() else Path(path.name)
92
+ relative = Path(prefix) / relative
93
+ content = path.read_bytes()
94
+ destination = self.directory / "inputs" / label / relative
95
+ destination.parent.mkdir(parents=True, exist_ok=True)
96
+ destination.write_bytes(content)
97
+ hashes[str(relative)] = hashlib.sha256(content).hexdigest()
98
+ self.manifest["inputs"][label] = hashes
99
+ self.save_manifest()
100
+
101
+ def artifact(self, name: str, value) -> Path:
102
+ root = self.directory / "artifacts"
103
+ path = root / name
104
+ if path.resolve() == root.resolve() or root.resolve() not in path.resolve().parents:
105
+ raise ValueError("Artifact path must stay inside the attempt")
106
+ path.parent.mkdir(parents=True, exist_ok=True)
107
+ write_json(path, value)
108
+ self.event(
109
+ "artifact",
110
+ path=str(path.relative_to(self.directory)),
111
+ sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
112
+ )
113
+ return path