Skip to content

Commit 54ef12c

Browse files
committed
fix
1 parent 1fb9d9b commit 54ef12c

File tree

1 file changed

+175
-48
lines changed

1 file changed

+175
-48
lines changed

packages/gg_api_core/src/gg_api_core/tools/remediate_secret_incidents.py

Lines changed: 175 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33

44
from pydantic import Field
55

6+
from developer_mcp_server.server import list_repo_occurrences
67
from .list_repo_incidents import list_repo_incidents
78

89
logger = logging.getLogger(__name__)
910

1011

11-
12-
1312
async def remediate_secret_incidents(
1413
repository_name: str = Field(
1514
description="The full repository name. For example, for https://github.com/GitGuardian/gg-mcp.git the full name is GitGuardian/gg-mcp. Pass the current repository name if not provided."
@@ -27,40 +26,52 @@ async def remediate_secret_incidents(
2726
),
2827
) -> dict[str, Any]:
2928
"""
30-
Find and remediate secret incidents in the current repository.
29+
Find and remediate secret incidents in the current repository using EXACT match locations.
3130
3231
By default, this tool only shows incidents assigned to the current user. Pass mine=False to get all incidents related to this repo.
3332
34-
This tool follows a workflow to:
35-
1. Use the provided repository name to search for incidents
36-
2. List secret occurrences for the repository
37-
3. Analyze and provide recommendations to remove secrets from the codebase
38-
4. IMPORTANT:Make the changes to the codebase to remove the secrets from the code using best practices for the language. All occurrences must not appear in the codebase anymore.
39-
IMPORTANT: If the repository is using a package manager like npm, cargo, uv or others, use it to install the required packages.
40-
5. Only optional: propose to rewrite git history
33+
This tool now uses the occurrences API to get precise file locations, line numbers, and character indices,
34+
eliminating the need to search for secrets in files. The workflow is:
35+
36+
1. Fetch secret occurrences with exact match locations (file path, line_start, line_end, index_start, index_end)
37+
2. Group occurrences by file for efficient remediation
38+
3. Sort matches from bottom to top to prevent line number shifts during editing
39+
4. Provide detailed remediation steps with exact locations for each secret
40+
5. IMPORTANT: Make the changes to the codebase using the provided indices:
41+
- Use index_start and index_end to locate the exact secret in the file
42+
- Replace hardcoded secrets with environment variable references
43+
- Ensure all occurrences are removed from the codebase
44+
- IMPORTANT: If the repository uses a package manager (npm, cargo, uv, etc.), use it to install required packages
45+
6. Optional: Generate git commands to rewrite history and remove secrets from git
4146
47+
The tool provides:
48+
- Exact file paths and line numbers for each secret
49+
- Character-level indices (index_start, index_end) to locate secrets precisely
50+
- Context lines (pre/post) to understand the surrounding code
51+
- Sorted matches to enable safe sequential removal (bottom-to-top)
4252
4353
Args:
44-
repository_name: The full repository name. For example, for https://github.com/GitGuardian/gg-mcp.git the full name is GitGuardian/gg-mcp
54+
repository_name: The full repository name (e.g., 'GitGuardian/gg-mcp')
4555
include_git_commands: Whether to include git commands to fix incidents in git history
4656
create_env_example: Whether to create a .env.example file with placeholders for detected secrets
47-
get_all: Whether to get all incidents or just the first page
48-
mine: If True, fetch only incidents assigned to the current user. Set to False to get all incidents.
57+
get_all: Whether to get all occurrences or just the first page
58+
mine: If True, fetch only occurrences for incidents assigned to the current user. Set to False to get all.
4959
5060
Returns:
5161
A dictionary containing:
52-
- repository_info: Information about the detected repository
53-
- incidents: List of detected incidents
54-
- remediation_steps: Steps to remediate the incidents
62+
- repository_info: Information about the repository
63+
- summary: Overview of occurrences, files affected, and secret types
64+
- remediation_steps: Detailed steps with exact locations for each file
65+
- env_example_content: Suggested .env.example content (if requested)
5566
- git_commands: Git commands to fix history (if requested)
5667
"""
57-
logger.debug(f"Using remediate_secret_incidents with sources API for: {repository_name}")
68+
logger.debug(f"Using remediate_secret_incidents with occurrences API for: {repository_name}")
5869

5970
try:
60-
incidents_result = await list_repo_incidents(
71+
# Get detailed occurrences with exact match locations
72+
occurrences_result = await list_repo_occurrences(
6173
repository_name=repository_name,
6274
get_all=get_all,
63-
mine=mine,
6475
# Explicitly pass None for optional parameters to avoid FieldInfo objects
6576
from_date=None,
6677
to_date=None,
@@ -71,23 +82,40 @@ async def remediate_secret_incidents(
7182
cursor=None,
7283
)
7384

74-
if "error" in incidents_result:
75-
return {"error": incidents_result["error"]}
85+
if "error" in occurrences_result:
86+
return {"error": occurrences_result["error"]}
7687

77-
incidents = incidents_result.get("incidents", [])
88+
occurrences = occurrences_result.get("occurrences", [])
7889

79-
if not incidents:
90+
# Filter by assignee if mine=True
91+
if mine:
92+
# Get current user info to filter by assignee
93+
client = mcp.get_client()
94+
try:
95+
token_info = await client.get_current_token_info()
96+
current_user_id = token_info.get("user_id") if token_info else None
97+
98+
if current_user_id:
99+
# Filter occurrences assigned to current user
100+
occurrences = [
101+
occ for occ in occurrences
102+
if occ.get("incident", {}).get("assignee_id") == current_user_id
103+
]
104+
logger.debug(f"Filtered to {len(occurrences)} occurrences assigned to user {current_user_id}")
105+
except Exception as e:
106+
logger.warning(f"Could not filter by assignee: {str(e)}")
107+
108+
if not occurrences:
80109
return {
81110
"repository_info": {"name": repository_name},
82-
"message": "No secret incidents found for this repository that match the criteria.",
111+
"message": "No secret occurrences found for this repository that match the criteria.",
83112
"remediation_steps": [],
84113
}
85114

86-
# Continue with remediation logic using the incidents...
87-
# This part is common between the optimized and fallback versions, so we can call a helper function
88-
logger.debug(f"Processing {len(incidents)} incidents for remediation")
89-
result = await _process_incidents_for_remediation(
90-
incidents=incidents,
115+
# Process occurrences for remediation with exact location data
116+
logger.debug(f"Processing {len(occurrences)} occurrences with exact locations for remediation")
117+
result = await _process_occurrences_for_remediation(
118+
occurrences=occurrences,
91119
repository_name=repository_name,
92120
include_git_commands=include_git_commands,
93121
create_env_example=create_env_example,
@@ -102,46 +130,145 @@ async def remediate_secret_incidents(
102130
return {"error": f"Failed to remediate incidents: {str(e)}"}
103131

104132

105-
async def _process_incidents_for_remediation(
106-
incidents: list[dict[str, Any]],
133+
async def _process_occurrences_for_remediation(
134+
occurrences: list[dict[str, Any]],
107135
repository_name: str,
108136
include_git_commands: bool = True,
109137
create_env_example: bool = True,
110138
) -> dict[str, Any]:
111139
"""
112-
Process incidents for remediation after they've been fetched.
140+
Process occurrences for remediation using exact match locations.
113141
114-
This helper function contains the shared logic for processing incidents
115-
and providing remediation steps.
142+
This function leverages the detailed location data from occurrences (file paths, line numbers,
143+
character indices) to provide precise remediation instructions without needing to search files.
116144
117145
Args:
118-
incidents: List of incidents to remediate
146+
occurrences: List of occurrences with exact match locations
119147
repository_name: Repository name
120148
include_git_commands: Whether to include git commands
121149
create_env_example: Whether to create .env.example
122150
123151
Returns:
124-
Remediation steps for each incident
152+
Remediation steps for each occurrence with exact file locations
125153
"""
126-
# For now, we'll just return the incidents list
127-
# In a real implementation, this would analyze the incidents and provide remediation steps
154+
# Group occurrences by file for efficient remediation
155+
occurrences_by_file = {}
156+
secret_types = set()
157+
affected_files = set()
158+
159+
for occurrence in occurrences:
160+
# Extract location data
161+
matches = occurrence.get("matches", [])
162+
incident_data = occurrence.get("incident", {})
163+
secret_type = incident_data.get("detector", {}).get("name", "Unknown")
164+
secret_types.add(secret_type)
165+
166+
for match in matches:
167+
file_path = match.get("match", {}).get("filename")
168+
if not file_path:
169+
continue
170+
171+
affected_files.add(file_path)
172+
173+
if file_path not in occurrences_by_file:
174+
occurrences_by_file[file_path] = []
175+
176+
# Store detailed match information
177+
match_info = {
178+
"occurrence_id": occurrence.get("id"),
179+
"incident_id": incident_data.get("id"),
180+
"secret_type": secret_type,
181+
"line_start": match.get("match", {}).get("line_start"),
182+
"line_end": match.get("match", {}).get("line_end"),
183+
"index_start": match.get("match", {}).get("index_start"),
184+
"index_end": match.get("match", {}).get("index_end"),
185+
"match_type": match.get("type"),
186+
"pre_line_start": match.get("pre_line_start"),
187+
"pre_line_end": match.get("pre_line_end"),
188+
"post_line_start": match.get("post_line_start"),
189+
"post_line_end": match.get("post_line_end"),
190+
}
191+
occurrences_by_file[file_path].append(match_info)
192+
193+
# Build remediation steps with exact locations
128194
remediation_steps = []
129-
for incident in incidents:
195+
196+
for file_path, matches in occurrences_by_file.items():
197+
# Sort matches by line number (descending) so we can remove from bottom to top
198+
# This prevents line number shifts when making multiple edits
199+
sorted_matches = sorted(matches, key=lambda m: m["line_start"] or 0, reverse=True)
200+
130201
step = {
131-
"incident_id": incident.get("id"),
132-
"secret_type": incident.get("type"),
202+
"file": file_path,
203+
"action": "remove_secrets",
204+
"matches": sorted_matches,
205+
"instructions": [
206+
f"File: {file_path}",
207+
f"Found {len(sorted_matches)} secret(s) in this file",
208+
"Matches are sorted from bottom to top for safe sequential removal",
209+
"",
210+
"For each match:",
211+
"1. Read the file content",
212+
f"2. Navigate to line {sorted_matches[0].get('line_start')} (and other match locations)",
213+
"3. Use the exact index_start and index_end to locate the secret",
214+
"4. Replace the hardcoded secret with an environment variable reference",
215+
"5. Ensure the secret is added to .env (gitignored) and .env.example (committed)",
216+
],
133217
"recommendations": [
134-
f"Remove the secret from {len(incident.get('repository_occurrences', []))} files",
135-
"Use environment variables instead of hardcoded secrets",
218+
"Replace secrets with environment variables (e.g., process.env.API_KEY, os.getenv('API_KEY'))",
219+
"Add the real secret to .env file (ensure .env is in .gitignore)",
220+
"Add a placeholder to .env.example for documentation",
221+
"Use a secrets management solution for production (e.g., AWS Secrets Manager, HashiCorp Vault)",
136222
],
137-
"include_git_commands": include_git_commands,
138-
"create_env_example": create_env_example,
139223
}
140224
remediation_steps.append(step)
141225

142-
return {
226+
# Generate .env.example content if requested
227+
env_example_content = None
228+
if create_env_example:
229+
env_vars = []
230+
for secret_type in secret_types:
231+
# Generate sensible environment variable names from secret types
232+
env_var_name = secret_type.upper().replace(" ", "_").replace("-", "_")
233+
env_vars.append(f"{env_var_name}=your_{secret_type.lower().replace(' ', '_')}_here")
234+
235+
if env_vars:
236+
env_example_content = "\n".join(env_vars)
237+
238+
# Generate git commands if requested
239+
git_commands = None
240+
if include_git_commands:
241+
git_commands = {
242+
"warning": "⚠️ These commands will rewrite git history. Only use if you understand the implications.",
243+
"commands": [
244+
"# First, ensure all secrets are removed from working directory",
245+
"git add .",
246+
'git commit -m "Remove hardcoded secrets"',
247+
],
248+
}
249+
250+
result = {
143251
"repository_info": {"name": repository_name},
144-
"incidents_count": len(incidents),
145-
"incidents": incidents,
252+
"summary": {
253+
"total_occurrences": len(occurrences),
254+
"affected_files": len(affected_files),
255+
"secret_types": list(secret_types),
256+
"files": list(affected_files),
257+
},
146258
"remediation_steps": remediation_steps,
147259
}
260+
261+
if env_example_content:
262+
result["env_example_content"] = env_example_content
263+
result["env_example_instructions"] = [
264+
"Create or update .env.example in your repository root:",
265+
f"```\n{env_example_content}\n```",
266+
"",
267+
"Ensure .env is in .gitignore:",
268+
"```\n.env\n```",
269+
]
270+
271+
if git_commands:
272+
result["git_commands"] = git_commands
273+
274+
return result

0 commit comments

Comments
 (0)