Sprint 6 Step 192: security diagnostics integration
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -55,6 +55,9 @@ private:
|
||||
static PrimitiveSymbol chooseFunction(const std::vector<PrimitiveSymbol>& 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;
|
||||
}
|
||||
|
||||
@@ -136,6 +136,19 @@
|
||||
return "";
|
||||
}
|
||||
|
||||
static bool securityDiagnosticAtLine(const std::vector<DiagnosticRange>& 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<DiagnosticRange>& 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,6 +35,7 @@ struct DependencyPanelState {
|
||||
bool needsIndex = false;
|
||||
bool vulnIgnoreLoaded = false;
|
||||
std::unordered_map<std::string, std::string> vulnIgnore;
|
||||
std::unordered_set<std::string> 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()) {
|
||||
|
||||
@@ -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<ImportLocation> collectImportLocations(const std::string& text,
|
||||
const std::string& language) {
|
||||
std::vector<ImportLocation> 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<EditorDiagnostic>& 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() {
|
||||
|
||||
@@ -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<std::string>& libs) {
|
||||
vulnerableLibraries_ = libs;
|
||||
}
|
||||
void setBlockVulnerableImports(bool block) { blockVulnerableImports_ = block; }
|
||||
|
||||
std::vector<PrimitiveSymbol> 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<std::string> vulnerableLibraries_;
|
||||
bool blockVulnerableImports_ = false;
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
31
editor/tests/step192_test.cpp
Normal file
31
editor/tests/step192_test.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
// Step 192: Security diagnostics integration.
|
||||
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
static std::string readFile(const std::string& path) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return {};
|
||||
return std::string((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user