-
Notifications
You must be signed in to change notification settings - Fork 229
Security status report SKILL #680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Michael Taufen (mtaufen)
wants to merge
4
commits into
agent-substrate:main
Choose a base branch
from
mtaufen:security-report
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| --- | ||
| name: security-status-report | ||
| description: Generates a security status report based on docs/threats.json by spinning up sub-agents for each threat to compute a quality score. | ||
| --- | ||
|
|
||
| # Task | ||
| Generate a security status report by evaluating threats listed in `docs/threats.json`. | ||
|
|
||
| # Workflow | ||
|
|
||
| 1. Copy `.agents/skills/security-status-report/scripts/template_dispatch.py` to `.agents/scratch/security-status-report/template_dispatch.py`. | ||
| 2. Complete the TODO in `.agents/scratch/security-status-report/template_dispatch.py`, meeting the following requirements: | ||
| a. Iterate over each threat from `docs/threats.json` to produce a list of invocations that matches your tool for invoking sub-agents. | ||
| b. Each invocation must address a single threat, use the below prompt, and instruct the sub-agent to output the correct schema. | ||
| 3. Execute the script from the repository root, specifying `.agents/scratch/security-status-report/subagents.json` as the output file argument (`python3 .agents/scratch/security-status-report/template_dispatch.py .agents/scratch/security-status-report/subagents.json`). | ||
| 4. Read `.agents/scratch/security-status-report/subagents.json` and copy its exact JSON array into your tool for invoking subagents. DO NOT manually craft or bypass the invocations. Run ALL generated sub-agents concurrently in a single tool call by default unless bound by model rate limits or harness concurrency limits (use batching intelligently if needed). You already updated the script in step 2 to output exactly what you need, so no modifications to the output should be necessary unless you made a mistake or hit limits. | ||
| 5. Wait for all sub-agents to complete. | ||
| 6. Run `python3 .agents/skills/security-status-report/scripts/compile_report.py docs/threats.json .agents/scratch/security-status-report .agents/scratch/security-status-report/final.json` to produce the final report. | ||
| 7. Run `python3 .agents/skills/security-status-report/scripts/render_chart.py .agents/scratch/security-status-report/final.json .agents/scratch/security-status-report/chart.png` to render a bar chart of the scores. | ||
|
|
102 changes: 102 additions & 0 deletions
102
.agents/skills/security-status-report/scripts/compile_report.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| # Copyright 2026 Google LLC | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| import sys | ||
| import json | ||
| import os | ||
|
|
||
| def compile_report(threats_json_path, results_dir, output_path): | ||
| with open(threats_json_path, 'r') as f: | ||
| threats = json.load(f) | ||
|
|
||
| final_report = [] | ||
| succeeded_count = 0 | ||
| failed_ids = [] | ||
|
|
||
| for t in threats: | ||
| threat_id = t.get("threat_id", "unknown") | ||
| result_file = os.path.join(results_dir, f"{threat_id}.json") | ||
|
|
||
| if os.path.exists(result_file): | ||
| try: | ||
| with open(result_file, 'r') as rf: | ||
| res = json.load(rf) | ||
|
|
||
| # Check threat_id matching | ||
| if not res.get("threat_id"): | ||
| t["error"] = f"Missing threat_id in result file" | ||
| elif res.get("threat_id") and res.get("threat_id") != threat_id: | ||
| t["error"] = f"Mismatched threat_id in result file: expected {threat_id}, got {res.get('threat_id')}" | ||
| else: | ||
| # Validate quality score presence, type, and range | ||
| if "quality" not in res: | ||
| t["error"] = "Result JSON missing 'quality' score" | ||
| elif isinstance(res["quality"], bool): | ||
| t["error"] = f"Invalid boolean quality score: {res['quality']}" | ||
| else: | ||
| try: | ||
| score = float(res["quality"]) | ||
| if not (0.0 <= score <= 1.0): | ||
| t["error"] = f"Quality score out of bounds [0.0, 1.0]: {res['quality']}" | ||
| else: | ||
| t["quality"] = score | ||
| except (ValueError, TypeError): | ||
| t["error"] = f"Invalid non-numeric quality score: {res['quality']}" | ||
|
|
||
| # Copy strengths and weaknesses if no quality error | ||
| if "error" not in t: | ||
| if "strengths" in res: | ||
| t["strengths"] = str(res["strengths"]) | ||
| if "weaknesses" in res: | ||
| t["weaknesses"] = str(res["weaknesses"]) | ||
|
|
||
| # Validate and normalize citations schema | ||
| if "citations" in res: | ||
| c = res["citations"] | ||
| if isinstance(c, list): | ||
| t["citations"] = [str(item) for item in c] | ||
| elif isinstance(c, str): | ||
| t["citations"] = [c] | ||
| else: | ||
| t["citations"] = [] | ||
| except Exception as e: | ||
| t["error"] = f"Failed to parse agent JSON: {e}" | ||
| else: | ||
| t["error"] = "Missing result file. The evaluation sub-agent may have timed out, failed to produce a valid JSON, or written to the wrong location." | ||
|
|
||
| if "error" in t: | ||
| failed_ids.append(threat_id) | ||
| else: | ||
| succeeded_count += 1 | ||
|
|
||
| final_report.append(t) | ||
|
|
||
| output_dir = os.path.dirname(output_path) | ||
| if output_dir: | ||
| os.makedirs(output_dir, exist_ok=True) | ||
| with open(output_path, 'w') as f: | ||
| json.dump(final_report, f, indent=2) | ||
|
|
||
| total_count = len(final_report) | ||
| print(f"Report compiled successfully to {output_path}") | ||
| print(f"Summary: {total_count} total threats | {succeeded_count} succeeded | {len(failed_ids)} failed") | ||
|
|
||
| if failed_ids: | ||
| print(f"Warning: The following {len(failed_ids)} threat(s) failed evaluation: {', '.join(failed_ids)}", file=sys.stderr) | ||
|
|
||
| if __name__ == '__main__': | ||
| if len(sys.argv) < 4: | ||
| print(f"Usage: {sys.argv[0]} <threats_json_path> <results_dir> <output_path>", file=sys.stderr) | ||
| sys.exit(1) | ||
| compile_report(sys.argv[1], sys.argv[2], sys.argv[3]) | ||
|
mtaufen marked this conversation as resolved.
|
||
114 changes: 114 additions & 0 deletions
114
.agents/skills/security-status-report/scripts/render_chart.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| # Copyright 2026 Google LLC | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| import sys | ||
| import json | ||
| import os | ||
|
|
||
| try: | ||
| import matplotlib | ||
| matplotlib.use('Agg') | ||
| import matplotlib.pyplot as plt | ||
| except ImportError: | ||
| print("Error: 'matplotlib' is required to render the chart. Please install it using 'pip install matplotlib'.", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| def render_chart(final_report_path, output_png_path): | ||
| with open(final_report_path, 'r') as f: | ||
| data = json.load(f) | ||
|
|
||
| # Sort data by threat_id (e.g. T-01, T-02, ...) | ||
| def sort_key(item): | ||
| tid = item.get("threat_id", "") | ||
| if tid.startswith("T-") and tid[2:].isdigit(): | ||
| return (0, int(tid[2:]), "") | ||
| return (1, 0, tid) | ||
|
|
||
| sorted_data = sorted(data, key=sort_key) | ||
|
|
||
| threat_ids = [item.get("threat_id", f"UNKNOWN-{i+1:02d}") for i, item in enumerate(sorted_data)] | ||
|
|
||
| scores = [] | ||
| errors = [] | ||
| bottom_colors = [] | ||
|
|
||
| for item in sorted_data: | ||
| raw_score = item.get("quality", 0.0) | ||
| is_error = "error" in item | ||
| errors.append(is_error) | ||
|
|
||
| try: | ||
| score = float(raw_score) if not is_error else 0.0 | ||
| except (ValueError, TypeError): | ||
| score = 0.0 | ||
| score = max(0.0, min(1.0, score)) | ||
| scores.append(score) | ||
|
|
||
| # These thresholds can be tweaked over time, they | ||
| # are NOT a policy, just a visual assist. | ||
| if score >= 0.95: | ||
| bottom_colors.append("#16a34a") # Green | ||
| elif score >= 0.5: | ||
| bottom_colors.append("#eab308") # Yellow | ||
| else: | ||
| bottom_colors.append("#f97316") # Orange | ||
|
|
||
| remaining_scores = [1.0 - s for s in scores] | ||
|
|
||
| fig, ax = plt.subplots(figsize=(14, 6)) | ||
| ax.bar(threat_ids, scores, color=bottom_colors, width=1.0) | ||
| ax.bar(threat_ids, remaining_scores, bottom=scores, color="#dc2626", width=1.0) | ||
|
|
||
| # Cap each bar with a thick black line for colorblind accessibility | ||
| x_positions = range(len(threat_ids)) | ||
| ax.hlines(y=scores, xmin=[x - 0.5 for x in x_positions], xmax=[x + 0.5 for x in x_positions], color='black', linewidth=3) | ||
|
|
||
| # Display exact score just below the black cap | ||
| for x, (s, err) in enumerate(zip(scores, errors)): | ||
| if not err: | ||
| y_pos = s - 0.01 | ||
| va = 'top' | ||
| if s < 0.05: | ||
| y_pos = s + 0.02 | ||
| va = 'bottom' | ||
| ax.text(x, y_pos, f"{s:.2f}", color='black', ha='center', va=va, fontsize=8, fontweight='bold', | ||
| bbox=dict(facecolor='white', alpha=0.7, edgecolor='none', pad=1.5)) | ||
|
|
||
| # Annotate errors with vertical "ERROR" text above x-axis | ||
| for i, err in enumerate(errors): | ||
| if err: | ||
| ax.text(i, 0.02, "ERROR", rotation=90, ha='center', va='bottom', fontsize=8, fontweight='bold', color='white') | ||
|
|
||
| ax.set_ylim(0.0, 1.0) | ||
| ax.set_xlim(-0.5, len(threat_ids) - 0.5) | ||
|
mtaufen marked this conversation as resolved.
|
||
| ax.set_ylabel("Quality Score (0.0 - 1.0)", fontsize=12, fontweight='bold') | ||
| ax.set_xlabel("Threat ID", fontsize=12, fontweight='bold') | ||
| ax.set_title("Substrate Security Threat Posture Scores", fontsize=16, fontweight='bold', pad=15) | ||
|
|
||
| # Standard matplotlib way to rotate tick labels cleanly | ||
| plt.xticks(rotation=90, fontsize=9, fontweight='bold') | ||
| ax.grid(axis='y', linestyle='--', alpha=0.5) | ||
|
|
||
| plt.tight_layout() | ||
| output_dir = os.path.dirname(output_png_path) | ||
| if output_dir: | ||
| os.makedirs(output_dir, exist_ok=True) | ||
| plt.savefig(output_png_path, dpi=150) | ||
| plt.close() | ||
| print(f"Chart rendered successfully to {output_png_path}") | ||
|
|
||
| if __name__ == '__main__': | ||
| report_file = sys.argv[1] if len(sys.argv) > 1 else ".agents/scratch/security-status-report/final.json" | ||
| output_png = sys.argv[2] if len(sys.argv) > 2 else ".agents/scratch/security-status-report/chart.png" | ||
| render_chart(report_file, output_png) | ||
69 changes: 69 additions & 0 deletions
69
.agents/skills/security-status-report/scripts/template_dispatch.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Copyright 2026 Google LLC | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """ | ||
| Template script for formatting threats into agent dispatch specifications. | ||
| The main agent should complete this script to output a valid JSON array | ||
| matching the 'Subagents' parameter of its invoke_subagent tool. | ||
| """ | ||
| import sys | ||
| import json | ||
| import os | ||
|
|
||
| def generate_dispatch_payload(output_path): | ||
| with open('docs/threats.json', 'r') as f: | ||
| threats = json.load(f) | ||
|
|
||
| subagents = [] | ||
|
|
||
| for t in threats: | ||
| prompt = f'''You are a security reviewer evaluating the following specific threat: | ||
|
|
||
| {json.dumps(t, indent=2)} | ||
|
|
||
| - Focus on this threat only. | ||
| - Review the entire repo. | ||
| - Produce a gut-feel "quality score" based on the current security posture of the repo with respect to that threat. | ||
| - Output your results using the following schema, by writing them to .agents/scratch/security-status-report/{t["threat_id"]}.json, | ||
| where {t["threat_id"]} matches the id in the threat json you were initially provided. | ||
|
|
||
| ```json | ||
| {{ | ||
| "threat_id": "<threat_id from input>", | ||
| "threat": "<threat text from input>", | ||
| "quality": <Decimal between 0 (no effective mitigation) and 1 (perfectly mitigated).>, | ||
| "strengths": "<Specific positive code/design mechanisms responsible for the score.>", | ||
| "weaknesses": "<Specific negative code/design mechanisms responsible for the score.>", | ||
| "citations": ["<repo-relative/path/to/file1.go>"] | ||
| }} | ||
| ```''' | ||
| # TODO: Agent, update this part to ensure it matches the correct schema for you to invoke sub-agents via a tool call. | ||
| subagents.append({ | ||
| "Prompt": prompt, | ||
| "Role": "Security Reviewer", | ||
| "TypeName": "self", | ||
| "Workspace": "inherit" | ||
| }) | ||
|
|
||
|
|
||
| output_dir = os.path.dirname(output_path) | ||
| if output_dir: | ||
| os.makedirs(output_dir, exist_ok=True) | ||
| with open(output_path, 'w') as f: | ||
| json.dump(subagents, f, indent=2) | ||
| print(f"Dispatch payload written successfully to {output_path}") | ||
|
|
||
| if __name__ == '__main__': | ||
| output_path = sys.argv[1] if len(sys.argv) > 1 else ".agents/scratch/security-status-report/subagents.json" | ||
| generate_dispatch_payload(output_path) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.