Skip to content

Commit efb5c48

Browse files
committed
Fix CHANGELOG gathering, improve output
1 parent cd124dd commit efb5c48

File tree

3 files changed

+120
-151
lines changed

3 files changed

+120
-151
lines changed

main.go

Lines changed: 43 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ func collectFromFiles(diffFile string) (*ucd.AnalysisData, error) {
131131
return &ucd.AnalysisData{
132132
VersionA: versionA,
133133
VersionB: versionB,
134+
Source: diffFile,
134135
Diff: string(diff),
135136
CommitMessages: string(commits),
136137
Changelog: string(changelog),
@@ -178,98 +179,64 @@ func outputJSON(result *ucd.Result) {
178179

179180
// outputText prints the result in human-readable format with colors and emojis.
180181
func outputText(r *ucd.Result) {
181-
// Setup formatters
182-
titleFmt := color.New(color.Bold, color.FgCyan).PrintlnFunc()
183-
sectionFmt := color.New(color.Bold, color.FgBlue).PrintlnFunc()
184-
highlight := color.New(color.Bold, color.FgYellow).SprintFunc()
185-
good := color.New(color.FgGreen).SprintFunc()
186-
warning := color.New(color.FgYellow).SprintFunc()
187-
danger := color.New(color.FgRed).SprintFunc()
188-
189-
titleFmt("✨ UCD: Undocumented Change Detector ✨")
190-
fmt.Printf("Comparing %s → %s\n\n", versionA, versionB)
191-
192-
// Helper functions for consistent formatting
193-
getRatingDisplay := func(rating int, isMalware bool) (string, func(a ...interface{}) string) {
194-
if isMalware {
195-
// Malware emojis
196-
switch {
197-
case rating <= 2:
198-
return "🔒", good
199-
case rating <= 6:
200-
return "⚠️", warning
201-
default:
202-
return "🚨", danger
203-
}
204-
} else {
205-
// Security patch emojis
206-
switch {
207-
case rating <= 2:
208-
return "🛡️ ", good
209-
case rating <= 6:
210-
return "🔧", warning
211-
default:
212-
return "🔓", danger
213-
}
182+
// Setup color formatters - Apple uses subtle colors
183+
title := color.New(color.FgHiBlue, color.Bold)
184+
section := color.New(color.FgBlue, color.Bold)
185+
highlight := color.New()
186+
// color.FgBlack, color.Bold) // Apple often uses bold black for emphasis
187+
success := color.New(color.FgHiGreen)
188+
warning := color.New(color.FgYellow)
189+
critical := color.New(color.FgHiRed)
190+
191+
// Print header in Apple style - clean and minimal
192+
title.Println("Undocumented Change Analysis")
193+
fmt.Printf("%s: %s → %s\n\n", r.Input.Source, versionA, versionB)
194+
195+
// Apple-style risk indicators - prefer text over emoji for enterprise tools
196+
riskLevel := func(level int) string {
197+
switch {
198+
case level <= 2:
199+
return success.Sprintf("Low (%d/10)", level)
200+
case level <= 6:
201+
return warning.Sprintf("Medium (%d/10)", level)
202+
default:
203+
return critical.Sprintf("High (%d/10)", level)
214204
}
215205
}
216206

217-
// Output summary if available
218207
if r.Summary != nil {
219-
sectionFmt("📊 RISK SUMMARY")
220-
221-
// Display malware risk
222-
malwareEmoji, malwareColor := getRatingDisplay(r.Summary.MalwareRisk, true)
223-
fmt.Printf("%s %s - malware\n",
224-
malwareColor(malwareEmoji),
225-
malwareColor(fmt.Sprintf("%d/10", r.Summary.MalwareRisk)))
226-
227-
// Display security patch risk
228-
securityEmoji, securityColor := getRatingDisplay(r.Summary.SilentPatch, false)
229-
fmt.Printf("%s %s - silent security patches\n",
230-
securityColor(securityEmoji),
231-
securityColor(fmt.Sprintf("%d/10", r.Summary.SilentPatch)))
232-
233-
fmt.Printf("\n%s\n\n", r.Summary.Description)
208+
section.Println("Risk Assessment")
209+
fmt.Printf("• Malicious Code: %s\n", riskLevel(r.Summary.MalwareRisk))
210+
fmt.Printf("• Silent Security Patch: %s\n", riskLevel(r.Summary.SilentPatch))
211+
fmt.Printf("• Summary: %s\n\n", r.Summary.Description)
234212
}
235213

236-
if len(r.Changes) == 0 {
237-
fmt.Println(good("✅ No undocumented behavioral changes found."))
214+
// No changes case - clean confirmation
215+
if len(r.UndocumentedChanges) == 0 {
216+
fmt.Println(success.Sprint("No undocumented changes detected."))
238217
return
239218
}
240219

241-
// Sort changes by maximum severity
242-
changes := r.Changes
243-
sort.Slice(changes, func(i, j int) bool {
244-
iMax := max(changes[i].MalwareRisk, changes[i].SilentPatch)
245-
jMax := max(changes[j].MalwareRisk, changes[j].SilentPatch)
246-
return iMax > jMax
220+
// Sort changes by severity
221+
sort.Slice(r.UndocumentedChanges, func(i, j int) bool {
222+
return max(r.UndocumentedChanges[i].MalwareRisk, r.UndocumentedChanges[i].SilentPatch) >
223+
max(r.UndocumentedChanges[j].MalwareRisk, r.UndocumentedChanges[j].SilentPatch)
247224
})
248225

249-
sectionFmt(fmt.Sprintf("🔍 UNDOCUMENTED BEHAVIOR CHANGES (%d found)", len(changes)))
226+
section.Printf("Undocumented Changes (%d)\n", len(r.UndocumentedChanges))
250227

251-
for _, change := range changes {
252-
// Print basic change information
253-
fmt.Printf("- %s\n", highlight(change.Description))
228+
for _, c := range r.UndocumentedChanges {
229+
fmt.Printf("• %s\n", highlight.Sprint(c.Description))
254230

255-
// Show malware risk if significant
256-
if change.MalwareRisk > 5 {
257-
malwareEmoji, malwareColor := getRatingDisplay(change.MalwareRisk, true)
258-
fmt.Printf(" %s %s %s\n",
259-
malwareEmoji,
260-
malwareColor(fmt.Sprintf("%d/10 malware risk:", change.MalwareRisk)),
261-
change.MalwareExplanation)
231+
if c.MalwareRisk > 3 {
232+
fmt.Printf(" • Malicious Code: %s\n %s\n",
233+
riskLevel(c.MalwareRisk), c.MalwareExplanation)
262234
}
263235

264-
// Show security patch risk if significant
265-
if change.SilentPatch > 5 {
266-
securityEmoji, securityColor := getRatingDisplay(change.SilentPatch, false)
267-
fmt.Printf(" %s %s %s\n",
268-
securityEmoji,
269-
securityColor(fmt.Sprintf("%d/10 hidden security patch:", change.SilentPatch)),
270-
change.SilentExplanation)
236+
if c.SilentPatch > 3 {
237+
fmt.Printf(" • Security Patch: %s\n %s\n",
238+
riskLevel(c.SilentPatch), c.SilentExplanation)
271239
}
272-
273240
}
274241
}
275242

pkg/ucd/analyze.go

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ type Assessment struct {
2424

2525
// Result contains the analysis findings.
2626
type Result struct {
27-
Changes []Assessment `json:"changes"`
28-
Summary *Assessment `json:"summary,omitempty"`
27+
Input *AnalysisData `json:"input"`
28+
UndocumentedChanges []Assessment `json:"undocumented_changes"`
29+
Summary *Assessment `json:"summary,omitempty"`
2930
}
3031

3132
// AnalyzeChanges performs AI-based analysis of code changes.
@@ -52,66 +53,55 @@ func AnalyzeChanges(ctx context.Context, client *genai.Client, data *AnalysisDat
5253

5354
responseText := string(resp.Candidates[0].Content.Parts[0].(genai.Text))
5455
r, err := parseAIResponse(responseText)
56+
if err != nil {
57+
return nil, fmt.Errorf("parse failure: %w", err)
58+
}
59+
r.Input = data
5560
klog.V(1).Infof("result: %+v", r)
5661
return r, err
5762
}
5863

5964
// buildPrompt constructs the prompt for the AI model.
6065
func buildPrompt(data *AnalysisData) (string, error) {
61-
prompt := fmt.Sprintf(`You are a security expert and malware analyst analyzing changes between two versions of a software package.
66+
prompt := fmt.Sprintf(`
67+
You are a security expert and malware analyst studying the changes between two versions of an
68+
open-source program that you are not familiar with.
69+
6270
I will provide:
63-
1. A unified diff between version %s and %s
71+
72+
1. A unified diff of changes between version %s and %s collected from %s
6473
2. Commit messages describing changes (if available)
6574
3. Changelog entries (if available)
6675
67-
Your task is to identify any behavioral changes in the code that do not appear to be related to changes mentioned in the commit messages or changelog.
68-
Be loose and liberal with your interpretation when relating code changes to a changelog entry or commit message.
69-
70-
We are trying to uncover two types of changes:
71-
72-
- Malicious changes being snuck into the supply change: for example, credential theft, backdoors, exfiltration, or data wipers
73-
- Silently fixed security vulnerabilities (CVE's), for example: directory traversal, buffer overflows
74-
75-
For each undocumented behavioral change you identify:
76-
1. Briefly describe the undocumented change in 15 words or less.
77-
2. Give the undocumented change a malice rating, from 0-10 (0=Benign, 5=Suspicious, 10=Extremely Dangerous)
78-
* Don't worry if the code is adding new functionality that may accidentally introduce a security vulnerability,
79-
such as potential code execution risk, but do care if the undocumented behaviors appear to be malicious, for example:
80-
adding a backdoor, downloading software, calling chmod to make programs executable, introducing malicious behaviors or add undocumented obfuscation to avoid code analysis.
81-
3. Give the undocumented change a silent security fix rating, based on how likely and critical you think the security patch might have been.
82-
If the code authors mention "security fix" or "CVE" in the changelog or commit messages relating to the delta between these two versions,
83-
it is less likely to see hidden silent security fixes.
84-
4. For each rating, provide a 1-sentence explanation of how you arrived to your conclusion.
76+
Your task is to determine if there are behavior changes present in the unified diff that are not documented
77+
by either the commit messages or changelog.
8578
86-
Thinking how a security engineer would reason about a combination of security threats or analyze software, you
87-
you also need to take a step back and consider the overall impact of all of the undocumented changes to assess a
88-
combined "malice" and "silent security fix" score.
79+
- Be loose in your interpretation of how a diff change
80+
may be related to a commit message or changelog entry.
81+
- Don't include undocumented code health improvements that often appear alongside feature changes.
82+
* For example, don't include documentation updates, changes that can come up in code refactoring, CI/CD configuration changes, or performance improvements.
83+
- Ignore changes to files within the .github directory, as they will not impact the users of this tool.
84+
- Unless you know of a specific security threat for a package version, assume that dependency version bumps are not part of a silent security fix.
8985
90-
In general, most software should score 0-1.
91-
92-
Here are undocumented behavioral changes to ignore:
93-
- Changes to .github/workflows/ files - as they do not impact the behavior of the software
94-
- Changes to documentation (.md files, for example) - as they do not impact the behavior of the software
95-
- Performance improvements
96-
- Changes that may be related to code refactoring
86+
Format your response as a JSON object with:
9787
98-
Focus on behavioral changes that could be construed as malicious or a fix for an undocumented critical security vulnerability.
88+
- "undocumented_changes": An array of JSON objects for each undocumented behavioral change that could impact a user of this program, each with:
89+
- "description": A concise 1-sentence description of the undocumented behavioral change
90+
- "malware_risk": 0-10 danger scale of this undocumented change being malicious in nature. For example, could this undocumented change
91+
represent the addition of code for credential exfiltration, a backdoor, or a data wiper? (0=Benign, 5=Suspicious, 10=Extremely Dangerous)
92+
- "malware_explanation": A 1-sentence explanation for the given malware_risk rating.
93+
- "silent_patch": 0-10 likelihood of this undocumented change representing a hidden critical security patch (0=Benign, 5=Suspicious, 10=Extremely Dangerous)
94+
- "silent_explanation": Your explanation for your silent_patch rating.
9995
100-
Format your response as a JSON object with:
101-
- "changes": An array of JSON objects, each with:
102-
- "description": A brief description of the undocumented change
103-
- "malware_risk": 0-10 danger scale of this change (0=Benign, 5=Suspicious, 10=Extremely Dangerous)
104-
- "malware_explanation": Your explanation for your malware risk rating.
105-
- "silent_patch": 0-10 likelihood of a silent critical security patch (0=Benign, 5=Suspicious, 10=Extremely Dangerous)
106-
- "silent_explanation": Your explanation for your silent_Patch rating.
107-
108-
- "summary": A JSON object that assesses the combined impact:
109-
- "description": A 1-sentence description of the combined undocumented behavioral changes.
96+
- "summary": A JSON object that assesses the combined impact of the undocumented behavioral changes you've found:
97+
- "description": A concise 1-sentence description of the combined undocumented behavioral changes.
11098
- "malware_risk": 0-10 danger scale of all combined changes considered together (0=Benign, 5=Suspicious, 10=Extremely Dangerous)
111-
- "malware_explanation": Your explanation for your combined malware risk rating.
99+
- "malware_explanation": A 1-sentence explanation for your combined malware risk rating.
112100
- "silent_patch": 0-10 likelihood of a silent critical security patch introduced in this version change (0=Benign, 5=Suspicious, 10=Extremely Dangerous)
113101
- "silent_explanation": Your explanation for your combined silent_patch rating.
114102
103+
Do not include changes mentioned in the Changelog or commit messages.
104+
115105
If there are no undocumented behavior changes, return an empty changes array. Your response must be in JSON form to be understood.
116106
117107
Here are the details to analyze:
@@ -124,7 +114,9 @@ COMMIT MESSAGES:
124114
125115
CHANGELOG CHANGES:
126116
%s
127-
`, data.VersionA, data.VersionB, data.Diff, data.CommitMessages, data.Changelog)
117+
118+
Ensure that the returned data is in valid JSON form.
119+
`, data.VersionA, data.VersionB, data.Source, data.Diff, data.CommitMessages, data.Changelog)
128120

129121
// Truncate if too long
130122
const maxPromptLength = 2000000
@@ -149,12 +141,15 @@ func parseAIResponse(response string) (*Result, error) {
149141
// Try to unmarshal as Result structure first
150142
var result Result
151143
err := json.Unmarshal([]byte(jsonText), &result)
144+
if err != nil {
145+
return nil, fmt.Errorf("unmarshal: %v\ncontent: %s", err, jsonText)
146+
}
152147
return &result, err
153148
}
154149

155150
// extractJSON retrieves JSON data from a response string.
156151
func extractJSON(response string) string {
157-
klog.Infof("response: %s", response)
152+
// klog.Infof("response: %s", response)
158153

159154
// Try code block first (most specific)
160155
codeBlockRegex := regexp.MustCompile("```(?:json)?\\n?(\\{.*?\\}|\\[.*?\\])\\n?```")

0 commit comments

Comments
 (0)