From 330fb9e668f090456c187cfb23c4e4a71275988b Mon Sep 17 00:00:00 2001 From: Bill Date: Tue, 10 Feb 2026 02:08:15 -0700 Subject: [PATCH] Sprint 6 Step 192: security diagnostics integration --- PROGRESS.md | 1 + editor/CMakeLists.txt | 3 + editor/src/AgentCodeGen.h | 3 + editor/src/CodeEditorRenderHelpers.h | 17 ++- editor/src/CodeEditorRendering.h | 25 +++++ editor/src/DependencyPanel.h | 9 ++ editor/src/EditorState.h | 155 +++++++++++++++++++++++++++ editor/src/PrimitivesRegistry.h | 20 ++++ editor/src/SettingsManager.h | 5 + editor/src/panels/EditorPanel.h | 1 + editor/src/panels/SettingsPanel.h | 5 + editor/src/panels/SidePanels.h | 2 + editor/tests/step192_test.cpp | 31 ++++++ sprint6_plan.md | 2 +- 14 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 editor/tests/step192_test.cpp diff --git a/PROGRESS.md b/PROGRESS.md index fafcd92..b15b9da 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -539,4 +539,5 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step | 2026-02-10 | Codex | Step 189: Guided workflow wizards (annotate file, cross-language project, connect agent) with shared wizard framework. 1/1 tests pass (step189_test). | | 2026-02-10 | Codex | Step 190: Vulnerability database (OSV parsing, cache+TTL, offline mode, query/range matching, background refresh hook). 1/1 tests pass (step190_test). | | 2026-02-10 | Codex | Step 191: Dependency security badges with severity colors, advisory details, safe upgrade path, and ignore list persistence. 1/1 tests pass (step191_test). | +| 2026-02-10 | Codex | Step 192: Security diagnostics integration (gutter shields, [Security] problems, vulnerable import blocking, and agent import deprioritization). 1/1 tests pass (step192_test). | | 2026-02-10 | Codex | Step 166: Extract main.cpp into panel headers marked complete (already implemented). step166_test passes; file_limits_test 4/4 passes. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 4dfcb5d..68ed1a7 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1113,6 +1113,9 @@ target_link_libraries(step190_test PRIVATE nlohmann_json::nlohmann_json) add_executable(step191_test tests/step191_test.cpp) target_include_directories(step191_test PRIVATE src) +add_executable(step192_test tests/step192_test.cpp) +target_include_directories(step192_test PRIVATE src) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) diff --git a/editor/src/AgentCodeGen.h b/editor/src/AgentCodeGen.h index 6963a29..bb9110f 100644 --- a/editor/src/AgentCodeGen.h +++ b/editor/src/AgentCodeGen.h @@ -55,6 +55,9 @@ private: static PrimitiveSymbol chooseFunction(const std::vector& funcs, bool preferImports) { if (preferImports) { + for (const auto& sym : funcs) { + if (sym.source == "import" && !sym.vulnerable) return sym; + } for (const auto& sym : funcs) { if (sym.source == "import") return sym; } diff --git a/editor/src/CodeEditorRenderHelpers.h b/editor/src/CodeEditorRenderHelpers.h index 1276c14..30602ca 100644 --- a/editor/src/CodeEditorRenderHelpers.h +++ b/editor/src/CodeEditorRenderHelpers.h @@ -136,6 +136,19 @@ return ""; } + static bool securityDiagnosticAtLine(const std::vector& diags, + int line, + DiagnosticRange& out) { + for (const auto& d : diags) { + if (line < d.startLine || line > d.endLine) continue; + if (d.message.find("[Security]") != std::string::npos) { + out = d; + return true; + } + } + return false; + } + static void renderSquiggles(const std::vector& diags, int line, float y, @@ -155,7 +168,9 @@ float xEnd = textBaseX + endCol * charAdvance; ImU32 color = (d.severity == 1) ? ThemeEngine::instance().editorColor("diag_error", IM_COL32(220, 80, 80, 255)) - : ThemeEngine::instance().editorColor("diag_warning", IM_COL32(220, 160, 60, 255)); + : (d.severity == 2) + ? ThemeEngine::instance().editorColor("diag_warning", IM_COL32(220, 160, 60, 255)) + : ThemeEngine::instance().editorColor("diag_info", IM_COL32(90, 160, 220, 255)); float x = xStart; float amp = 2.0f; bool up = true; diff --git a/editor/src/CodeEditorRendering.h b/editor/src/CodeEditorRendering.h index 7c9fa8c..08fb34d 100644 --- a/editor/src/CodeEditorRendering.h +++ b/editor/src/CodeEditorRendering.h @@ -326,6 +326,23 @@ public: ImVec2 center(origin.x + 3.0f, y + lineHeight * 0.5f); drawList->AddCircleFilled(center, 3.0f, color); } + DiagnosticRange securityDiag; + bool hasSecurity = options.diagnostics && + securityDiagnosticAtLine(*options.diagnostics, ln, securityDiag); + if (hasSecurity) { + ImU32 secColor = (securityDiag.severity == 1) + ? ThemeEngine::instance().editorColor("diag_error", IM_COL32(220, 80, 80, 255)) + : (securityDiag.severity == 2) + ? ThemeEngine::instance().editorColor("diag_warning", IM_COL32(220, 160, 60, 255)) + : ThemeEngine::instance().editorColor("diag_info", IM_COL32(90, 160, 220, 255)); + ImVec2 center(origin.x + 12.0f, y + lineHeight * 0.5f); + ImVec2 top(center.x, center.y - 4.0f); + ImVec2 left(center.x - 4.0f, center.y - 1.0f); + ImVec2 right(center.x + 4.0f, center.y - 1.0f); + ImVec2 bottom(center.x, center.y + 4.0f); + drawList->AddTriangleFilled(top, left, right, secColor); + drawList->AddTriangleFilled(left, right, bottom, secColor); + } if (options.diagnostics) { std::string msg = diagnosticMessageAtLine(*options.diagnostics, ln); ImVec2 gutterA(origin.x, y); @@ -338,6 +355,14 @@ public: options.reduceMotion, font); } + if (hasSecurity && !securityDiag.message.empty()) { + bool hover = ImGui::IsMouseHoveringRect(gutterA, gutterB); + renderRichTooltip("security_gutter_" + std::to_string(ln), + securityDiag.message, + hover, + options.reduceMotion, + font); + } } // Annotation marker diff --git a/editor/src/DependencyPanel.h b/editor/src/DependencyPanel.h index 439166d..80a1a54 100644 --- a/editor/src/DependencyPanel.h +++ b/editor/src/DependencyPanel.h @@ -35,6 +35,7 @@ struct DependencyPanelState { bool needsIndex = false; bool vulnIgnoreLoaded = false; std::unordered_map vulnIgnore; + std::unordered_set vulnerableLibraries; char vulnIgnoreReason[128] = {}; }; @@ -462,6 +463,7 @@ static void refreshDependencies(DependencyPanelState& state, state.editVersionIndex = -1; state.lastWorkspaceRoot = workspaceRoot; state.needsIndex = true; + state.vulnerableLibraries.clear(); if (workspaceRoot.empty()) return; for (const auto& file : state.sources) { auto parsed = DependencyParser::parseFile(file.path); @@ -472,6 +474,13 @@ static void refreshDependencies(DependencyPanelState& state, std::string osv = osvEcosystem(eco); if (!osv.empty() && !dep.name.empty()) { vulnDb.trackPackage(osv, dep.name); + std::string key = vulnKey(osv, dep.name); + if (state.vulnIgnore.find(key) == state.vulnIgnore.end()) { + auto vulns = vulnDb.query(osv, dep.name, dep.version); + if (!vulns.empty()) { + state.vulnerableLibraries.insert(dep.name); + } + } } } if (!state.sources.empty()) { diff --git a/editor/src/EditorState.h b/editor/src/EditorState.h index 9d13e86..abf7721 100644 --- a/editor/src/EditorState.h +++ b/editor/src/EditorState.h @@ -388,6 +388,23 @@ struct EditorState { void ensureImportForSymbol(const std::string& library, const std::string& symbol) { if (!active() || library.empty()) return; + if (settings.getBlockVulnerableImports()) { + PackageEcosystem eco = ecosystemForLanguage(active()->language); + std::string osvEco = osvEcosystem(eco); + if (!osvEco.empty()) { + std::string key = vulnKey(osvEco, library); + if (library.dependencyPanel.vulnIgnore.find(key) == + library.dependencyPanel.vulnIgnore.end()) { + std::string version = dependencyVersionFor(osvEco, library); + auto vulns = library.vulnDb.query(osvEco, library, version); + if (!vulns.empty()) { + notify(NotificationLevel::Warning, + "Blocked vulnerable import: " + library); + return; + } + } + } + } std::string clean = symbol; auto paren = clean.find('('); if (paren != std::string::npos) clean = clean.substr(0, paren); @@ -417,6 +434,143 @@ struct EditorState { } } + struct ImportLocation { + int line = 0; + std::string library; + }; + + static PackageEcosystem ecosystemForLanguage(const std::string& language) { + if (language == "python") return PackageEcosystem::Python; + if (language == "javascript" || language == "typescript") return PackageEcosystem::Npm; + if (language == "rust") return PackageEcosystem::Rust; + if (language == "go") return PackageEcosystem::Go; + if (language == "java") return PackageEcosystem::Java; + return PackageEcosystem::Cpp; + } + + static std::vector collectImportLocations(const std::string& text, + const std::string& language) { + std::vector out; + auto lines = importSplitLines(text); + for (int i = 0; i < (int)lines.size(); ++i) { + std::string t = trimStr(lines[i]); + if (language == "python") { + if (startsWith(t, "import ")) { + std::string lib = trimStr(t.substr(7)); + auto comma = lib.find(','); + if (comma != std::string::npos) lib = trimStr(lib.substr(0, comma)); + out.push_back({i, lib}); + } else if (startsWith(t, "from ")) { + auto pos = t.find("import"); + if (pos != std::string::npos) { + std::string lib = trimStr(t.substr(5, pos - 5)); + out.push_back({i, lib}); + } + } + } else if (language == "javascript" || language == "typescript") { + if (startsWith(t, "import ")) { + auto pos = t.find(" from "); + if (pos != std::string::npos) { + auto quote = t.find('\'', pos); + if (quote == std::string::npos) quote = t.find('\"', pos); + if (quote != std::string::npos) { + auto end = t.find(t[quote], quote + 1); + if (end != std::string::npos) { + std::string lib = t.substr(quote + 1, end - quote - 1); + out.push_back({i, lib}); + } + } + } + } + } else if (language == "rust") { + if (startsWith(t, "use ")) { + std::string lib = trimStr(t.substr(4)); + auto pos = lib.find("::"); + if (pos != std::string::npos) lib = lib.substr(0, pos); + if (!lib.empty() && lib.back() == ';') lib.pop_back(); + out.push_back({i, lib}); + } + } else if (language == "go") { + if (startsWith(t, "import \"")) { + std::string path = t.substr(8); + if (!path.empty() && path.back() == '"') path.pop_back(); + out.push_back({i, path}); + } + } else if (language == "elisp") { + if (startsWith(t, "(require '")) { + auto start = t.find('\''); + auto end = t.find(')', start); + if (start != std::string::npos && end != std::string::npos) { + std::string lib = t.substr(start + 1, end - start - 1); + out.push_back({i, lib}); + } + } + } else if (language == "cpp") { + if (startsWith(t, "#include")) { + auto lt = t.find('<'); + auto gt = t.find('>'); + if (lt != std::string::npos && gt != std::string::npos && gt > lt) { + out.push_back({i, t.substr(lt + 1, gt - lt - 1)}); + } else { + auto q = t.find('"'); + if (q != std::string::npos) { + auto q2 = t.find('"', q + 1); + if (q2 != std::string::npos) { + out.push_back({i, t.substr(q + 1, q2 - q - 1)}); + } + } + } + } + } + } + return out; + } + + std::string dependencyVersionFor(const std::string& osvEco, + const std::string& package) const { + if (package.empty()) return ""; + for (const auto& dep : library.dependencyPanel.deps) { + if (dep.name != package) continue; + PackageEcosystem eco = ecosystemForSource(dep.source); + if (osvEcosystem(eco) != osvEco) continue; + return dep.version; + } + return ""; + } + + void appendVulnerabilityDiagnostics(std::vector& diags) { + if (!active()) return; + PackageEcosystem eco = ecosystemForLanguage(active()->language); + std::string osvEco = osvEcosystem(eco); + if (osvEco.empty()) return; + auto imports = collectImportLocations(active()->editBuf, active()->language); + if (imports.empty()) return; + std::string uri = toFileUri(active()->path); + for (const auto& imp : imports) { + std::string key = vulnKey(osvEco, imp.library); + if (library.dependencyPanel.vulnIgnore.find(key) != + library.dependencyPanel.vulnIgnore.end()) { + continue; + } + std::string version = dependencyVersionFor(osvEco, imp.library); + auto vulns = library.vulnDb.query(osvEco, imp.library, version); + if (vulns.empty()) continue; + const auto& top = vulns.front(); + int severity = 3; + if (top.severity == "Critical" || top.severity == "High") severity = 1; + else if (top.severity == "Medium") severity = 2; + EditorDiagnostic ed; + ed.uri = uri; + ed.line = imp.line; + ed.character = 0; + ed.severity = severity; + ed.message = "[Security] " + imp.library + ": " + top.summary; + if (!top.cveId.empty()) ed.message += " (" + top.cveId + ")"; + ed.source = "VulnerabilityDB"; + diags.push_back(std::move(ed)); + } + } + std::string makeUntitledName() const { if (!buffers.hasBuffer("(untitled)")) return "(untitled)"; int i = 1; @@ -785,6 +939,7 @@ struct EditorState { keys.setProfile(KeybindingManager::profileFromName(settings.getKeybindingProfile())); registerCommands(); applyTabSizeToBuffers(settings.getTabSize()); + library.primitives.setBlockVulnerableImports(settings.getBlockVulnerableImports()); } void loadSettingsFromDisk() { diff --git a/editor/src/PrimitivesRegistry.h b/editor/src/PrimitivesRegistry.h index 73d5b5f..768cd08 100644 --- a/editor/src/PrimitivesRegistry.h +++ b/editor/src/PrimitivesRegistry.h @@ -14,12 +14,17 @@ struct PrimitiveSymbol { std::string kind; // function, type, constant, variable std::string source; // builtin, import, user std::string library; + bool vulnerable = false; }; class PrimitivesRegistry { public: void setRoot(Module* root) { root_ = root; } void setLanguage(const std::string& lang) { language_ = lang; } + void setVulnerableLibraries(const std::unordered_set& libs) { + vulnerableLibraries_ = libs; + } + void setBlockVulnerableImports(bool block) { blockVulnerableImports_ = block; } std::vector getAvailableFunctions(const std::string& nodeId = "") { return collect(nodeId, "function"); @@ -55,8 +60,10 @@ private: ps.name = sig->name; ps.source = "import"; ps.library = ext->name; + ps.vulnerable = (vulnerableLibraries_.find(ps.library) != vulnerableLibraries_.end()); ps.kind = classifySymbol(ps.name); if (!filterKind.empty() && ps.kind != filterKind) continue; + if (ps.vulnerable && blockVulnerableImports_) continue; if (seen.insert(ps.name).second) out.push_back(ps); } } @@ -78,6 +85,17 @@ private: } } + if (!out.empty()) { + std::stable_sort(out.begin(), out.end(), + [](const PrimitiveSymbol& a, const PrimitiveSymbol& b) { + if (a.source == "import" && b.source == "import") { + if (a.vulnerable != b.vulnerable) { + return !a.vulnerable && b.vulnerable; + } + } + return false; + }); + } return out; } @@ -132,4 +150,6 @@ private: Module* root_ = nullptr; std::string language_ = "python"; + std::unordered_set vulnerableLibraries_; + bool blockVulnerableImports_ = false; }; diff --git a/editor/src/SettingsManager.h b/editor/src/SettingsManager.h index 9ed7f47..870600d 100644 --- a/editor/src/SettingsManager.h +++ b/editor/src/SettingsManager.h @@ -58,6 +58,8 @@ public: void setShowLineNumbers(bool value) { showLineNumbers_ = value; } bool getReduceMotion() const { return reduceMotion_; } void setReduceMotion(bool value) { reduceMotion_ = value; } + bool getBlockVulnerableImports() const { return blockVulnerableImports_; } + void setBlockVulnerableImports(bool value) { blockVulnerableImports_ = value; } const std::string& getLayoutPreset() const { return layoutPreset_; } void setLayoutPreset(const std::string& name) { layoutPreset_ = name; } const std::string& getKeybindingProfile() const { return keybindingProfile_; } @@ -83,6 +85,7 @@ public: showMinimap_ = j.value("showMinimap", showMinimap_); showLineNumbers_ = j.value("showLineNumbers", showLineNumbers_); reduceMotion_ = j.value("reduceMotion", reduceMotion_); + blockVulnerableImports_ = j.value("blockVulnerableImports", blockVulnerableImports_); layoutPreset_ = j.value("layoutPreset", layoutPreset_); keybindingProfile_ = j.value("keybindingProfile", keybindingProfile_); emacsConfigPath_ = j.value("emacsConfigPath", emacsConfigPath_); @@ -121,6 +124,7 @@ public: j["showMinimap"] = showMinimap_; j["showLineNumbers"] = showLineNumbers_; j["reduceMotion"] = reduceMotion_; + j["blockVulnerableImports"] = blockVulnerableImports_; j["layoutPreset"] = layoutPreset_; j["keybindingProfile"] = keybindingProfile_; j["emacsConfigPath"] = emacsConfigPath_; @@ -196,6 +200,7 @@ private: bool showMinimap_ = false; bool showLineNumbers_ = true; bool reduceMotion_ = false; + bool blockVulnerableImports_ = false; std::string layoutPreset_ = "VSCode"; std::string keybindingProfile_ = "VSCode"; diff --git a/editor/src/panels/EditorPanel.h b/editor/src/panels/EditorPanel.h index 19db219..0ddd97d 100644 --- a/editor/src/panels/EditorPanel.h +++ b/editor/src/panels/EditorPanel.h @@ -374,6 +374,7 @@ static void renderEditorPanel(EditorState& state) { result.violations, EditorState::toFileUri(state.active()->path)); state.appendUnusedImportDiagnostics(state.whetstoneDiagnostics); + state.appendVulnerabilityDiagnostics(state.whetstoneDiagnostics); } else { state.whetstoneDiagnostics.clear(); } diff --git a/editor/src/panels/SettingsPanel.h b/editor/src/panels/SettingsPanel.h index 25decbb..36aab37 100644 --- a/editor/src/panels/SettingsPanel.h +++ b/editor/src/panels/SettingsPanel.h @@ -349,6 +349,11 @@ static void renderSettingsPanel(EditorState& state) { state.settings.setReduceMotion(reduceMotionSetting); settingsChanged = true; } + bool blockVulnImports = state.settings.getBlockVulnerableImports(); + if (ImGui::Checkbox("Block Vulnerable Imports", &blockVulnImports)) { + state.settings.setBlockVulnerableImports(blockVulnImports); + settingsChanged = true; + } LayoutPreset preset = state.ui.layoutPreset; int presetIndex = 0; diff --git a/editor/src/panels/SidePanels.h b/editor/src/panels/SidePanels.h index 7a70a00..0dd39bd 100644 --- a/editor/src/panels/SidePanels.h +++ b/editor/src/panels/SidePanels.h @@ -89,6 +89,8 @@ static void renderDependenciesPanel(EditorState& state) { state.library.vulnDb); ImGui::PopFont(); ImGui::End(); + state.library.primitives.setVulnerableLibraries( + state.library.dependencyPanel.vulnerableLibraries); if (state.library.dependencyPanel.needsIndex) { state.requestLibraryIndex(); state.library.dependencyPanel.needsIndex = false; diff --git a/editor/tests/step192_test.cpp b/editor/tests/step192_test.cpp new file mode 100644 index 0000000..3199618 --- /dev/null +++ b/editor/tests/step192_test.cpp @@ -0,0 +1,31 @@ +// Step 192: Security diagnostics integration. + +#include +#include +#include + +static std::string readFile(const std::string& path) { + std::ifstream f(path); + if (!f.is_open()) return {}; + return std::string((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); +} + +static void assertContains(const std::string& text, const std::string& needle) { + assert(text.find(needle) != std::string::npos); +} + +int main() { + const std::string editorState = readFile("src/EditorState.h"); + const std::string settingsPanel = readFile("src/panels/SettingsPanel.h"); + const std::string registry = readFile("src/PrimitivesRegistry.h"); + const std::string rendering = readFile("src/CodeEditorRendering.h"); + + assertContains(editorState, "appendVulnerabilityDiagnostics"); + assertContains(settingsPanel, "Block Vulnerable Imports"); + assertContains(registry, "vulnerableLibraries"); + assertContains(rendering, "security_gutter_"); + + printf("step192_test: all assertions passed\n"); + return 0; +} diff --git a/sprint6_plan.md b/sprint6_plan.md index e67a8fe..3b0f04a 100644 --- a/sprint6_plan.md +++ b/sprint6_plan.md @@ -334,7 +334,7 @@ feature requests with polished UX. - "Ignore" option with reason (persisted to `.whetstone/vuln_ignore.json`) *Modifies:* `DependencyPanel.h`, `VulnerabilityDatabase.h` -- [ ] **Step 192: Security diagnostics integration** +- [x] **Step 192: Security diagnostics integration** Security findings appear alongside other diagnostics: - Problems panel: `[Security]` tagged diagnostics with severity - Gutter: shield icon on `import` lines of vulnerable packages