From 90b3ef1489e2c9b1376fd5c12bf5a8c03026819e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Rocha?= Date: Mon, 19 Jan 2026 17:40:13 +0000 Subject: [PATCH 1/2] Backport features already on codacy-semgrep --- docs/codacy-rules-ai.yaml | 24 ++++ docs/codacy-rules-i18n.yaml | 127 +++++++++++++++++ docs/multiple-tests/ai/patterns.xml | 6 + docs/multiple-tests/ai/results.xml | 9 ++ .../multiple-tests/ai/src/cs/GeminiExample.cs | 17 +++ docs/multiple-tests/i18n/patterns.xml | 7 + docs/multiple-tests/i18n/results.xml | 49 +++++++ .../i18n/src/Messages_en.properties | 6 + .../i18n/src/Messages_fr.properties | 6 + docs/multiple-tests/i18n/src/Order.cpp | 92 ++++++++++++ docs/multiple-tests/i18n/src/OrderApp.java | 32 +++++ .../i18n/src/OrderController.java | 30 ++++ docs/multiple-tests/i18n/src/OrderList.js | 46 ++++++ .../multiple-tests/i18n/src/OrderService.java | 27 ++++ docs/multiple-tests/i18n/src/Orderlist.jsx | 70 ++++++++++ .../i18n/src/PaymentService.java | 23 +++ docs/multiple-tests/i18n/src/UILayer.java | 25 ++++ docs/multiple-tests/i18n/src/i18n.js | 51 +++++++ internal/docgen/parsing.go | 131 ++++++++++++++++-- internal/docgen/pattern-with-explanation.go | 3 + internal/tool/configuration.go | 111 ++++++++++++++- 21 files changed, 875 insertions(+), 17 deletions(-) create mode 100644 docs/codacy-rules-ai.yaml create mode 100644 docs/codacy-rules-i18n.yaml create mode 100644 docs/multiple-tests/ai/patterns.xml create mode 100644 docs/multiple-tests/ai/results.xml create mode 100644 docs/multiple-tests/ai/src/cs/GeminiExample.cs create mode 100644 docs/multiple-tests/i18n/patterns.xml create mode 100644 docs/multiple-tests/i18n/results.xml create mode 100644 docs/multiple-tests/i18n/src/Messages_en.properties create mode 100644 docs/multiple-tests/i18n/src/Messages_fr.properties create mode 100644 docs/multiple-tests/i18n/src/Order.cpp create mode 100644 docs/multiple-tests/i18n/src/OrderApp.java create mode 100644 docs/multiple-tests/i18n/src/OrderController.java create mode 100644 docs/multiple-tests/i18n/src/OrderList.js create mode 100644 docs/multiple-tests/i18n/src/OrderService.java create mode 100644 docs/multiple-tests/i18n/src/Orderlist.jsx create mode 100644 docs/multiple-tests/i18n/src/PaymentService.java create mode 100644 docs/multiple-tests/i18n/src/UILayer.java create mode 100644 docs/multiple-tests/i18n/src/i18n.js diff --git a/docs/codacy-rules-ai.yaml b/docs/codacy-rules-ai.yaml new file mode 100644 index 0000000..d58dc0b --- /dev/null +++ b/docs/codacy-rules-ai.yaml @@ -0,0 +1,24 @@ +rules: + - id: codacy.csharp.ai.insecure-llm-model-usage + languages: + - csharp + message: "Usage of Insecure LLM Model: $MODEL" + severity: ERROR + patterns: + - pattern-either: + - pattern: | + $CLIENT.GenerateContentAsync(..., model: "$MODEL", ...) + - pattern: | + $CLIENT.GenerateContentAsync(model: "$MODEL", ...) + - metavariable-regex: + metavariable: $MODEL + regex: + metadata: + category: security + subcategory: ai + description: Detects usage of insecure/unauthorized LLM models in C# codebases + technology: + - csharp + impact: MEDIUM + confidence: LOW + likelihood: MEDIUM \ No newline at end of file diff --git a/docs/codacy-rules-i18n.yaml b/docs/codacy-rules-i18n.yaml new file mode 100644 index 0000000..cba4477 --- /dev/null +++ b/docs/codacy-rules-i18n.yaml @@ -0,0 +1,127 @@ +rules: + - id: codacy.java.i18n.enforce-localized-output + severity: WARNING + languages: + - java + patterns: + - pattern-either: + # Detect direct string literals + - pattern: System.out.println("..."); + - pattern: System.out.print("..."); + - pattern: System.err.println("..."); + - pattern: System.err.print("..."); + # Detect string concatenation + - pattern: System.out.println($X + ...); + - pattern: System.out.print($X + ...); + - pattern: System.err.println($X + ...); + - pattern: System.err.print($X + ...); + # Detect String.format without ResourceBundle + - pattern: System.out.println(String.format(...)); + - pattern: System.out.print(String.format(...)); + - pattern-not: System.out.println($BUNDLE.getString(...)) + - pattern-not: System.out.print($BUNDLE.getString(...)) + - pattern-not: System.err.println($BUNDLE.getString(...)) + - pattern-not: System.err.print($BUNDLE.getString(...)) + - pattern-not: System.out.println($BUNDLE.getObject(...)) + - pattern-not: System.out.print($BUNDLE.getObject(...)) + # Allow println without arguments (blank lines) + - pattern-not: System.out.println() + - pattern-not: System.err.println() + message: >- + Use localized messages instead of hardcoded strings. + System.out.println() should use ResourceBundle.getString() or equivalent localization method. + Example: System.out.println(messages.getString("key")) where messages is of type java.util.ResourceBundle + metadata: + category: codestyle + subcategory: i18n + description: Enforces use of ResourceBundle for all user-facing output to ensure proper internationalization + technology: + - java + impact: MEDIUM + confidence: LOW + likelihood: HIGH + + - id: codacy.js.i18n.no-hardcoded-alert-concat + severity: WARNING + languages: + - js + - ts + pattern-either: + # Direct hardcoded alert strings + - pattern: alert("...") + - pattern: window.alert("...") + # String concatenation in alerts + - pattern: alert("..." + ...) + - pattern: alert(... + "...") + - pattern: window.alert("..." + ...) + - pattern: window.alert(... + "...") + pattern-not: alert(t(...)) + message: >- + Avoid hardcoded or concatenated strings in alerts. Use an i18n translation function (e.g., t("key")) with interpolation. + metadata: + category: codestyle + subcategory: i18n + description: Flags hardcoded and concatenated strings in alert dialogs to enforce localization + technology: + - javascript + - typescript + impact: MEDIUM + confidence: LOW + likelihood: HIGH + + - id: codacy.js.i18n.no-hardcoded-locale-date + severity: WARNING + languages: + - js + - ts + pattern-regex: "\\.(toLocale(Date|Time)?String)\\(\"[^\"]+\"" + message: Avoid hardcoded locale strings in date/time formatting. + metadata: + category: codestyle + subcategory: i18n + description: Flags explicit locale strings in date/time formatting which can break localization + technology: + - javascript + - typescript + impact: MEDIUM + confidence: LOW + likelihood: HIGH + + - id: codacy.js.i18n.no-hardcoded-number-format + severity: WARNING + languages: + - js + - ts + pattern-regex: "\\.toFixed\\([^)]*\\)" + message: >- + Avoid using toFixed for user-visible number formatting. Use locale-aware formatting or translation helpers. + metadata: + category: codestyle + subcategory: i18n + description: Flags toFixed used for UI number formatting; recommends locale-aware alternatives + technology: + - javascript + - typescript + impact: MEDIUM + confidence: LOW + likelihood: HIGH + + - id: codacy.js.i18n.no-raw-jsx-text + severity: WARNING + languages: + - js + - ts + pattern-regex: "<(h1|h2|h3|h4|h5|h6|p|span|div|td|th)[^>]*>[^<{]*[A-Za-z][^<{]*" + message: >- + Avoid raw text in JSX for user-facing content. Use i18n translation functions (e.g., t("key")) with interpolation. + metadata: + category: codestyle + subcategory: i18n + description: Flags raw text nodes in JSX elements to enforce localization of UI strings + technology: + - javascript + - typescript + impact: MEDIUM + confidence: LOW + likelihood: MEDIUM + \ No newline at end of file diff --git a/docs/multiple-tests/ai/patterns.xml b/docs/multiple-tests/ai/patterns.xml new file mode 100644 index 0000000..e87962a --- /dev/null +++ b/docs/multiple-tests/ai/patterns.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/multiple-tests/ai/results.xml b/docs/multiple-tests/ai/results.xml new file mode 100644 index 0000000..cf09e18 --- /dev/null +++ b/docs/multiple-tests/ai/results.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/docs/multiple-tests/ai/src/cs/GeminiExample.cs b/docs/multiple-tests/ai/src/cs/GeminiExample.cs new file mode 100644 index 0000000..b487990 --- /dev/null +++ b/docs/multiple-tests/ai/src/cs/GeminiExample.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using Google.GenAI; +using Google.GenAI.Types; + +public class GenerateContentSimpleText { + public static async Task main() { + // The client gets the API key from the environment variable `GEMINI_API_KEY`. + var client = new Client(); + var response = await client.Models.GenerateContentAsync( + model: "deepseek-v3.2", contents: "Explain how AI works in a few words" + ); + var response2 = await client.Models.GenerateContentAsync( + model: "gemini-2.5-flash", contents: "Explain how AI works in a few words" + ); + Console.WriteLine(response.Candidates[0].Content.Parts[0].Text); + } +} \ No newline at end of file diff --git a/docs/multiple-tests/i18n/patterns.xml b/docs/multiple-tests/i18n/patterns.xml new file mode 100644 index 0000000..823ff8c --- /dev/null +++ b/docs/multiple-tests/i18n/patterns.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/docs/multiple-tests/i18n/results.xml b/docs/multiple-tests/i18n/results.xml new file mode 100644 index 0000000..a1bdcb0 --- /dev/null +++ b/docs/multiple-tests/i18n/results.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/multiple-tests/i18n/src/Messages_en.properties b/docs/multiple-tests/i18n/src/Messages_en.properties new file mode 100644 index 0000000..859ad83 --- /dev/null +++ b/docs/multiple-tests/i18n/src/Messages_en.properties @@ -0,0 +1,6 @@ +app.start=Welcome to the Internationalized Order System +order.processing=Processing order for {0} with {1} items. +order.success=Order placed successfully for {0}! +payment.success=Payment of {1} processed for customer {0}. +error.payment=Payment could not be processed. Please try again. +button.cancel=Cancel diff --git a/docs/multiple-tests/i18n/src/Messages_fr.properties b/docs/multiple-tests/i18n/src/Messages_fr.properties new file mode 100644 index 0000000..3c859f0 --- /dev/null +++ b/docs/multiple-tests/i18n/src/Messages_fr.properties @@ -0,0 +1,6 @@ +app.start=Bienvenue dans le système de commande internationalisé +order.processing=Traitement de la commande pour {0} avec {1} articles. +order.success=Commande passée avec succès pour {0}! +payment.success=Paiement de {1} traité pour le client {0}. +error.payment=Le paiement n'a pas pu être traité. Veuillez réessayer. +button.cancel=Annuler diff --git a/docs/multiple-tests/i18n/src/Order.cpp b/docs/multiple-tests/i18n/src/Order.cpp new file mode 100644 index 0000000..44026b5 --- /dev/null +++ b/docs/multiple-tests/i18n/src/Order.cpp @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#include // for number formatting + +struct Order { + int id; + std::string customer; + int quantity; + std::string status; + double price; +}; + +class OrderManager { + std::vector orders; + int nextId = 1; + +public: + void createOrder(const std::string& customer, int qty, double price) { + Order o{nextId++, customer, qty, "NEW", price}; + orders.push_back(o); + + // ❌ Hardcoded success message + std::cout << "Order created successfully for customer: " + << customer << " with quantity " << qty + << " and price " << price << std::endl; + } + + void listOrders() { + std::cout << "------ Order List ------" << std::endl; // ❌ Hardcoded label + + for (auto& o : orders) { + std::cout << "Order ID: " << o.id << ", " + << "Customer: " << o.customer << ", " + << "Qty: " << o.quantity << ", " + // ❌ Hardcoded status mapping + << "Status: " << (o.status == "NEW" ? "New Order" : o.status) << ", " + // ❌ Locale-unaware currency formatting + << "Price: $" << std::fixed << std::setprecision(2) << o.price + << std::endl; + } + + std::cout << "------ End of Orders ------" << std::endl; // ❌ Hardcoded footer + } + + void deleteOrder(int id) { + for (auto it = orders.begin(); it != orders.end(); ++it) { + if (it->id == id) { + orders.erase(it); + // ❌ Hardcoded delete confirmation + std::cout << "Order deleted successfully!" << std::endl; + return; + } + } + // ❌ Hardcoded error message + std::cout << "Error: Order not found." << std::endl; + } + + void printReport() { + // ❌ Locale-unaware date formatting (fixed US-style format) + std::time_t now = std::time(nullptr); + char buffer[80]; + std::strftime(buffer, sizeof(buffer), "%m/%d/%Y %H:%M:%S", std::localtime(&now)); + std::cout << "Report generated at: " << buffer << std::endl; + + // ❌ Hardcoded label + locale-unaware number formatting + double revenue = 0; + for (auto& o : orders) { + revenue += o.price * o.quantity; + } + + std::cout << "Total Orders: " << orders.size() << std::endl; + std::cout << "Total Revenue: " << revenue << std::endl; // ❌ Missing locale formatting + } +}; + +int main() { + OrderManager manager; + + manager.createOrder("Alice", 3, 1234.56); + manager.createOrder("Bob", 5, 98765.43); + + manager.listOrders(); + + manager.deleteOrder(2); + manager.deleteOrder(10); // should print error + + manager.printReport(); + + return 0; +} diff --git a/docs/multiple-tests/i18n/src/OrderApp.java b/docs/multiple-tests/i18n/src/OrderApp.java new file mode 100644 index 0000000..66f81b6 --- /dev/null +++ b/docs/multiple-tests/i18n/src/OrderApp.java @@ -0,0 +1,32 @@ +import java.util.Locale; +import java.util.ResourceBundle; + +public class OrderApp { + private static final ResourceBundle messages = ResourceBundle.getBundle("Messages", Locale.ENGLISH); + + public static void main(String[] args) { + UILayer ui = new UILayer(messages); + ui.showWelcome(); + + OrderService orderService = new OrderService(messages); + PaymentService paymentService = new PaymentService(messages); + + // GOOD: Using localization + orderService.processOrder("Order123", 3); + + // BAD: Hardcoded UI label + System.out.println("=== ORDER SUMMARY ==="); // ❌ should be localized + + // GOOD: Localized + ui.showLabel("button.cancel"); + + // Simulate payment success + paymentService.processPayment("Carlos", 150.0, true); + + // Simulate payment failure + paymentService.processPayment("Marie", 99.9, false); + + // BAD: Inline error + System.out.println("Unable to generate invoice. Try again later."); // ❌ + } +} diff --git a/docs/multiple-tests/i18n/src/OrderController.java b/docs/multiple-tests/i18n/src/OrderController.java new file mode 100644 index 0000000..135b18b --- /dev/null +++ b/docs/multiple-tests/i18n/src/OrderController.java @@ -0,0 +1,30 @@ +// OrderController.java (excerpt with intentional i18n gaps) + +@PostMapping +public ResponseEntity createOrder(@Valid @RequestBody Order order) { + order.setId(idGen.incrementAndGet()); + orders.add(order); + + // ❌ Hardcoded success message (i18n gap) + Map response = new HashMap<>(); + response.put("message", "Order created successfully!"); + response.put("order", order); + return ResponseEntity.status(HttpStatus.CREATED).body(response); +} + +@PutMapping("/{id}") +public ResponseEntity updateOrder(@PathVariable Long id, @Valid @RequestBody Order order) { + Optional existing = orders.stream().filter(o -> o.getId().equals(id)).findFirst(); + if (existing.isPresent()) { + Order o = existing.get(); + o.setCustomerName(order.getCustomerName()); + o.setQuantity(order.getQuantity()); + o.setStatus(order.getStatus()); + + // ❌ Hardcoded update message + return ResponseEntity.ok(Map.of("message", "Order updated successfully", "order", o)); + } + + // ❌ Hardcoded error message + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", "Order not found")); +} diff --git a/docs/multiple-tests/i18n/src/OrderList.js b/docs/multiple-tests/i18n/src/OrderList.js new file mode 100644 index 0000000..7bf205c --- /dev/null +++ b/docs/multiple-tests/i18n/src/OrderList.js @@ -0,0 +1,46 @@ +// OrderList.js (excerpt with intentional i18n gaps) + +import React, { useEffect, useState } from "react"; + +export default function OrderList() { + const [orders, setOrders] = useState([]); + + useEffect(() => { + fetch("http://localhost:8080/api/orders") + .then((res) => res.json()) + .then(setOrders); + }, []); + + return ( +
+ {/* ❌ Hardcoded title */} +

Order Management

+ + + + + + + {/* ❌ Hardcoded column headers */} + + + + + + + {orders.map((o) => ( + + + + {/* ❌ Hardcoded status mapping */} + + + ))} + +
Customer NameQuantityStatus
{o.customerName}{o.quantity}{o.status === "NEW" ? "New Order" : o.status}
+
+ ); +} diff --git a/docs/multiple-tests/i18n/src/OrderService.java b/docs/multiple-tests/i18n/src/OrderService.java new file mode 100644 index 0000000..e1a7e83 --- /dev/null +++ b/docs/multiple-tests/i18n/src/OrderService.java @@ -0,0 +1,27 @@ +import java.text.MessageFormat; +import java.util.ResourceBundle; + +public class OrderService { + private final ResourceBundle messages; + + public OrderService(ResourceBundle messages) { + this.messages = messages; + } + + public void processOrder(String customer, int count) { + // BAD: Concatenated string + System.out.println("Processing order for " + customer + " with " + count + " items."); // ❌ + + // GOOD: Localized + String msg = messages.getString("order.processing"); + System.out.println(MessageFormat.format(msg, customer, count)); + + if (count > 2) { + String success = messages.getString("order.success"); + System.out.println(MessageFormat.format(success, customer)); + } else { + // BAD: Hardcoded success + System.out.println("Order placed successfully!"); // ❌ + } + } +} diff --git a/docs/multiple-tests/i18n/src/Orderlist.jsx b/docs/multiple-tests/i18n/src/Orderlist.jsx new file mode 100644 index 0000000..7a8ddba --- /dev/null +++ b/docs/multiple-tests/i18n/src/Orderlist.jsx @@ -0,0 +1,70 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; + +export default function OrderList() { + const { t, i18n } = useTranslation(); + const [orders, setOrders] = useState([ + { id: 1, customer: "Alice", amount: 1234.56, date: new Date() }, + { id: 2, customer: "Bob", amount: 98765.43, date: new Date() }, + ]); + + const switchLang = (lng) => i18n.changeLanguage(lng); + + const addOrder = () => { + // ❌ BAD: Hardcoded + concatenation (gap) + alert("Order for " + "Charlie" + " created on " + new Date()); + + // ✅ GOOD: Use i18n key + interpolation + // alert(t("order.success")); + }; + + return ( +
+

{t("order.title")}

+
+ + + +
+ + + + + + + + + + + + + {orders.map((o) => ( + + + + {/* ❌ BAD: Hardcoded date formatting */} + + + {/* ✅ GOOD: i18n aware date */} + {/* */} + + {/* ❌ BAD: Hardcoded number */} + + + {/* ✅ GOOD: Locale-sensitive */} + {/* */} + + ))} + +
{t("order.customer")}{t("order.date")}{t("order.amount")}
{o.customer}{o.date.toLocaleDateString("en-US")}{t("order.date", { date: o.date, format: "date" })}${o.amount.toFixed(2)}{t("order.amount", { amount: o.amount, format: "number" })}
+ +

{t("order.total", { count: orders.length })}

+ + {/* ❌ BAD: Revenue displayed without locale formatting */} +

Total Revenue: {orders.reduce((sum, o) => sum + o.amount, 0)}

+ + {/* ✅ GOOD: Revenue with i18n interpolation */} + {/*

{t("order.revenue", { revenue: orders.reduce((s, o) => s + o.amount, 0), format: "number" })}

*/} +
+ ); +} diff --git a/docs/multiple-tests/i18n/src/PaymentService.java b/docs/multiple-tests/i18n/src/PaymentService.java new file mode 100644 index 0000000..b5db9f0 --- /dev/null +++ b/docs/multiple-tests/i18n/src/PaymentService.java @@ -0,0 +1,23 @@ +import java.text.MessageFormat; +import java.util.ResourceBundle; + +public class PaymentService { + private final ResourceBundle messages; + + public PaymentService(ResourceBundle messages) { + this.messages = messages; + } + + public void processPayment(String customer, double amount, boolean success) { + if (success) { + String msg = messages.getString("payment.success"); + System.out.println(MessageFormat.format(msg, customer, amount)); + } else { + // BAD: Inline error + System.out.println("Payment failed for " + customer + "! Amount: " + amount); // ❌ + + // GOOD: Localized + System.out.println(messages.getString("error.payment")); + } + } +} diff --git a/docs/multiple-tests/i18n/src/UILayer.java b/docs/multiple-tests/i18n/src/UILayer.java new file mode 100644 index 0000000..725d3af --- /dev/null +++ b/docs/multiple-tests/i18n/src/UILayer.java @@ -0,0 +1,25 @@ +import java.util.ResourceBundle; + +public class UILayer { + private final ResourceBundle messages; + + public UILayer(ResourceBundle messages) { + this.messages = messages; + } + + public void showWelcome() { + // BAD: Hardcoded welcome + System.out.println("Welcome to Order Processing System"); // ❌ + + // GOOD: Localized welcome + System.out.println(messages.getString("app.start")); + } + + public void showLabel(String key) { + try { + System.out.println(messages.getString(key)); + } catch (Exception e) { + System.out.println("Missing i18n key: " + key); // ❌ fallback + } + } +} diff --git a/docs/multiple-tests/i18n/src/i18n.js b/docs/multiple-tests/i18n/src/i18n.js new file mode 100644 index 0000000..f29f5aa --- /dev/null +++ b/docs/multiple-tests/i18n/src/i18n.js @@ -0,0 +1,51 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; + +const resources = { + en: { + translation: { + "order.title": "Order Management", + "order.create": "Create Order", + "order.customer": "Customer", + "order.date": "Order Date", + "order.amount": "Amount", + "order.success": "Order created successfully", + "order.error": "Failed to create order", + "order.total": "Total Orders: {{count}}", + "order.revenue": "Total Revenue: {{revenue, number}}", + }, + }, + fr: { + translation: { + "order.title": "Gestion des commandes", + // missing "order.success" -> should fallback + "order.create": "Créer une commande", + }, + }, + pseudo: { + translation: new Proxy({}, { + get: (_, key) => `[[${key}]]`, // Pseudo-localization + }), + }, +}; + +i18n.use(initReactI18next).init({ + resources, + lng: "en", // default + fallbackLng: "en", // fallback + debug: true, + interpolation: { + escapeValue: false, + format: function (value, format) { + if (format === "number") { + return new Intl.NumberFormat(i18n.language).format(value); + } + if (format === "date") { + return new Intl.DateTimeFormat(i18n.language).format(value); + } + return value; + }, + }, +}); + +export default i18n; diff --git a/internal/docgen/parsing.go b/internal/docgen/parsing.go index 013641c..ef7cf09 100644 --- a/internal/docgen/parsing.go +++ b/internal/docgen/parsing.go @@ -10,10 +10,13 @@ import ( "sort" "strings" + codacy "github.com/codacy/codacy-engine-golang-seed/v6" "github.com/samber/lo" "gopkg.in/yaml.v3" ) +var htmlCommentRegex = regexp.MustCompile(``) + // Downloads Semgrep rules from the official repository. // Downloads the default rules from the Registry. // Parses Semgrep rules from YAML files. @@ -24,11 +27,12 @@ type SemgrepConfig struct { } type SemgrepRule struct { - ID string `yaml:"id"` - Message string `yaml:"message"` - Severity string `yaml:"severity"` - Languages []string `yaml:"languages"` - Metadata SemgrepRuleMetadata `yaml:"metadata"` + ID string `yaml:"id"` + Message string `yaml:"message"` + Severity string `yaml:"severity"` + Languages []string `yaml:"languages"` + Metadata SemgrepRuleMetadata `yaml:"metadata"` + Parameters []codacy.PatternParameter } type SemgrepRuleMetadata struct { @@ -80,6 +84,7 @@ func semgrepRules(destinationDir string) ([]PatternWithExplanation, *ParsedSemgr allRulesFiles := append(parsedSemgrepRegistryRules.Files, parsedGitLabRules.Files...) allRulesFiles = append(allRulesFiles, parsedCodacyRules.Files...) + parsedRules := ParsedSemgrepRules{ Rules: allRules, Files: allRulesFiles, @@ -106,12 +111,33 @@ func getGitLabRules() (*ParsedSemgrepRules, error) { } func getCodacyRules(docsDir string) (*ParsedSemgrepRules, error) { - filePath, _ := filepath.Abs(path.Join(docsDir, "codacy-rules.yaml")) - return getRules( - filePath, - "", - func(_ string) bool { return true }, - func(_ string, unprefixedID string) string { return unprefixedID }) + + parsedRules := &ParsedSemgrepRules{ + Rules: SemgrepRules{}, + Files: []SemgrepRuleFile{}, + IDMapper: map[IDMapperKey]string{}, + } + customRulesFiles := []string{ + "codacy-rules.yaml", + "codacy-rules-i18n.yaml", + "codacy-rules-ai.yaml", + } + for _, file := range customRulesFiles { + filePath, _ := filepath.Abs(path.Join(docsDir, file)) + rules, err := getRules( + filePath, + "", + func(_ string) bool { return true }, + func(_ string, unprefixedID string) string { return unprefixedID }) + if err != nil { + return nil, err + } + parsedRules.Rules = append(parsedRules.Rules, rules.Rules...) + parsedRules.Files = append(parsedRules.Files, rules.Files...) + maps.Copy(parsedRules.IDMapper, rules.IDMapper) + } + return parsedRules, nil + } type FilenameValidator func(string) bool @@ -220,10 +246,7 @@ func prefixRuleIDWithPath(relativePath string, unprefixedID string) string { filename := filepath.Base(relativePath) filenameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename)) prefixedID := strings.ReplaceAll(filepath.Dir(relativePath), "/", ".") + "." + filenameWithoutExt + "." + unprefixedID - - // Remove leading "tmp." if present (unconditionally) - lowerPrefixedID := strings.ToLower(prefixedID) - return lowerPrefixedID + return strings.ToLower(prefixedID) } func getSemgrepRegistryDefaultRules() (SemgrepRules, error) { @@ -242,16 +265,93 @@ func readRulesFromYaml(yamlFile string) ([]SemgrepRule, error) { return nil, &DocGenError{msg: fmt.Sprintf("Failed to read file: %s", yamlFile), w: err} } + // First unmarshal to raw map to extract regex patterns + var rawConfig map[string]interface{} + err = yaml.Unmarshal(buf, &rawConfig) + if err != nil { + return nil, &DocGenError{msg: fmt.Sprintf("Failed to unmarshal file: %s", yamlFile), w: err} + } + + // Then unmarshal to structured config c := &SemgrepConfig{} err = yaml.Unmarshal(buf, c) if err != nil { return nil, &DocGenError{msg: fmt.Sprintf("Failed to unmarshal file: %s", yamlFile), w: err} + } + // Extract parameters from regex placeholders + if rawRules, ok := rawConfig["rules"].([]interface{}); ok { + for i, rawRule := range rawRules { + if i >= len(c.Rules) { + break + } + if ruleMap, ok := rawRule.(map[string]interface{}); ok { + c.Rules[i].Parameters = extractParametersFromRule(ruleMap) + } + } } return c.Rules, nil } +// extractParametersFromRule recursively searches for regex fields with HTML comment placeholders +// and creates PatternParameters for each one found +func extractParametersFromRule(ruleMap map[string]interface{}) []codacy.PatternParameter { + var parameters []codacy.PatternParameter + seenParams := make(map[string]bool) + + var searchForRegex func(obj interface{}) + searchForRegex = func(obj interface{}) { + switch v := obj.(type) { + case map[string]interface{}: + for key, value := range v { + if key == "regex" { + if regexStr, ok := value.(string); ok { + if matches := htmlCommentRegex.FindStringSubmatch(regexStr); len(matches) > 1 { + paramName := matches[1] + if !seenParams[paramName] { + seenParams[paramName] = true + // Convert to proper case (e.g., MODEL_REGEX -> modelRegex) + formattedName := formatParameterName(paramName) + parameters = append(parameters, codacy.PatternParameter{ + Name: formattedName, + Description: fmt.Sprintf("Regular expression pattern for %s", strings.ToLower(strings.ReplaceAll(paramName, "_", " "))), + Default: ".*", + }) + } + } + } + } else { + searchForRegex(value) + } + } + case []interface{}: + for _, item := range v { + searchForRegex(item) + } + } + } + + searchForRegex(ruleMap) + return parameters +} + +// formatParameterName converts UPPER_CASE to camelCase +func formatParameterName(name string) string { + parts := strings.Split(strings.ToLower(name), "_") + if len(parts) == 0 { + return name + } + + result := parts[0] + for i := 1; i < len(parts); i++ { + if len(parts[i]) > 0 { + result += strings.ToUpper(string(parts[i][0])) + parts[i][1:] + } + } + return result +} + func (r SemgrepRule) toPatternWithExplanation() PatternWithExplanation { return PatternWithExplanation{ ID: r.ID, @@ -264,6 +364,7 @@ func (r SemgrepRule) toPatternWithExplanation() PatternWithExplanation { Languages: toCodacyLanguages(r), Enabled: isEnabledByDefault(r), Explanation: r.Message, + Parameters: r.Parameters, } } diff --git a/internal/docgen/pattern-with-explanation.go b/internal/docgen/pattern-with-explanation.go index bb6fa30..4bb9bf6 100644 --- a/internal/docgen/pattern-with-explanation.go +++ b/internal/docgen/pattern-with-explanation.go @@ -17,6 +17,7 @@ type PatternWithExplanation struct { Languages []string Enabled bool Explanation string + Parameters []codacy.PatternParameter } func (r PatternWithExplanation) toCodacyPattern() codacy.Pattern { @@ -28,6 +29,7 @@ func (r PatternWithExplanation) toCodacyPattern() codacy.Pattern { Level: string(r.Level), Languages: r.Languages, Enabled: r.Enabled, + Parameters: r.Parameters, } } @@ -36,6 +38,7 @@ func (r PatternWithExplanation) toCodacyPatternDescription() codacy.PatternDescr PatternID: r.ID, Description: r.Description, Title: r.Title, + Parameters: r.Parameters, } } diff --git a/internal/tool/configuration.go b/internal/tool/configuration.go index d4a1ae4..8bb2346 100644 --- a/internal/tool/configuration.go +++ b/internal/tool/configuration.go @@ -2,16 +2,20 @@ package tool import ( "bufio" + "fmt" "io/fs" "os" "path" "path/filepath" + "regexp" "strings" codacy "github.com/codacy/codacy-engine-golang-seed/v6" "github.com/samber/lo" ) +var htmlCommentRegex = regexp.MustCompile(``) + const sourceConfigurationFileName = ".semgrep.yaml" // TODO: should respect cli flag for docs location @@ -25,12 +29,14 @@ func newConfigurationFile(toolExecution codacy.ToolExecution) (*os.File, error) if sourceConfigurationFileExists(toolExecution.SourceDir) { return getSourceConfigurationFile(toolExecution.SourceDir) } + return createConfigurationFileFromDefaultPatterns(*toolExecution.ToolDefinition.Patterns) } if len(*toolExecution.Patterns) == 0 { return nil, nil } + // if there are configured patterns, create a configuration file from them return createConfigurationFileFromPatterns(toolExecution.Patterns) } @@ -96,12 +102,15 @@ func createAndWriteConfigurationFile(scanner *bufio.Scanner, patterns *[]codacy. } idIsPresent := false + var currentPattern *codacy.Pattern for scanner.Scan() { line := scanner.Text() - idIsPresent = defaultRuleIsConfigured(line, patterns, idIsPresent) + idIsPresent, currentPattern = defaultRuleIsConfiguredWithPattern(line, patterns, idIsPresent, currentPattern) if idIsPresent { - _, err := configurationFile.WriteString(line + "\n") + // Replace HTML comment placeholders with parameter values + processedLine := replaceParameterPlaceholders(line, currentPattern) + _, err := configurationFile.WriteString(processedLine + "\n") if err != nil { return nil, err } @@ -118,6 +127,20 @@ func defaultRuleIsConfigured(line string, patterns *[]codacy.Pattern, idIsPresen return idIsPresent // We want to keep the same value } +func defaultRuleIsConfiguredWithPattern(line string, patterns *[]codacy.Pattern, idIsPresent bool, currentPattern *codacy.Pattern) (bool, *codacy.Pattern) { + if strings.Contains(line, "- id:") { + id := strings.TrimSpace(strings.Split(line, ":")[1]) + pattern, found := lo.Find(*patterns, func(item codacy.Pattern) bool { + return item.ID == id + }) + if found { + return true, &pattern + } + return false, nil + } + return idIsPresent, currentPattern +} + func isIDPresent(id string, patterns *[]codacy.Pattern) bool { _, res := lo.Find(*patterns, func(item codacy.Pattern) bool { return item.ID == id @@ -125,6 +148,90 @@ func isIDPresent(id string, patterns *[]codacy.Pattern) bool { return res } +// replaceParameterPlaceholders replaces HTML comment placeholders (e.g., ) +// with the corresponding parameter values from the pattern +func replaceParameterPlaceholders(line string, pattern *codacy.Pattern) string { + if pattern == nil || len(pattern.Parameters) == 0 { + return line + } + + // Check if line contains an HTML comment placeholder + if !htmlCommentRegex.MatchString(line) { + return line + } + + // Replace each HTML comment with the corresponding parameter value + result := htmlCommentRegex.ReplaceAllStringFunc(line, func(match string) string { + matches := htmlCommentRegex.FindStringSubmatch(match) + if len(matches) > 1 { + paramName := matches[1] + // Convert UPPER_CASE to camelCase to match parameter name format + formattedParamName := formatParameterName(paramName) + // Find the parameter in the pattern + for _, param := range pattern.Parameters { + + if param.Name == formattedParamName { + // Use Value if set, otherwise use Default + value := param.Value + if value == nil { + value = param.Default + } + if value != nil { + valueStr := fmt.Sprintf("%v", value) + + // If parameter name ends with _ALLOW_LIST, convert comma-separated list to regex pattern + if strings.HasSuffix(paramName, "_ALLOW_LIST") { + return convertListToRegex(valueStr, false) + } + return valueStr + } + } + } + } + // If no parameter found, keep the original placeholder + return match + }) + + return result +} + +// convertListToRegex converts a comma-separated list into a regex alternation pattern +// Example: "gemini-2.5-flash,gpt-3.5-turbo,old-llama-model" -> "^(gemini-2\\.5-flash|gpt-3\\.5-turbo|old-llama-model)$" +func convertListToRegex(list string, include bool) string { + // Split by comma and trim spaces + items := strings.Split(list, ",") + for i, item := range items { + // Trim whitespace + item = strings.TrimSpace(item) + // Escape dots for regex + item = strings.ReplaceAll(item, ".", "\\.") + items[i] = item + } + + // Join with pipe separator and wrap in regex anchors + if include { + return fmt.Sprintf("^(%s)$", strings.Join(items, "|")) + } + + return fmt.Sprintf("^(?!(%s)$).*", strings.Join(items, "|")) +} + +// formatParameterName converts UPPER_CASE to camelCase +func formatParameterName(name string) string { + parts := strings.Split(strings.ToLower(name), "_") + if len(parts) == 0 { + return name + } + + result := parts[0] + for i := 1; i < len(parts); i++ { + if len(parts[i]) > 0 { + result += strings.ToUpper(string(parts[i][0])) + parts[i][1:] + } + } + return result +} + var filesByLanguage = make(map[string][]string) // Semgrep: supported language tags are: apex, bash, c, c#, c++, cairo, clojure, cpp, csharp, dart, docker, dockerfile, elixir, ex, generic, go, golang, hack, hcl, html, java, javascript, js, json, jsonnet, julia, kotlin, kt, lisp, lua, none, ocaml, php, promql, proto, proto3, protobuf, py, python, python2, python3, r, regex, ruby, rust, scala, scheme, sh, sol, solidity, swift, terraform, tf, ts, typescript, vue, xml, yaml From e1a5813f810601d56820dec09ad39d5230f249e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Rocha?= Date: Wed, 21 Jan 2026 10:45:19 +0000 Subject: [PATCH 2/2] bump opengrep to 1.15.1 --- .tool_version | 2 +- Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.tool_version b/.tool_version index 63f522f..795d870 100644 --- a/.tool_version +++ b/.tool_version @@ -1 +1 @@ -1.11.5 \ No newline at end of file +1.15.1 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c531bd2..2edc0c7 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG OPENGREP_VERSION=v1.11.5 +ARG OPENGREP_VERSION=v1.15.1 # Build codacy-opengrep wrapper FROM golang:1.23-alpine3.21 as builder