Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions desloppify/languages/cxx/cppcheck_diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Distinguish cppcheck setup/parse failures from reliable code diagnostics."""

from __future__ import annotations

from dataclasses import replace
from pathlib import Path

from desloppify.languages._framework.generic_parts.tool_runner import ToolRunResult

_CONFIGURATION_IDS = frozenset({"missingInclude", "missingIncludeSystem", "unknownMacro"})


def configuration_checked_result(result: ToolRunResult) -> ToolRunResult:
"""Fail the invocation when cppcheck could not understand its translation units.

Cppcheck can exit zero while reporting missing headers or unsupported macros.
The security runner retries individual files and preserves regex security
fallback; returning an error here activates that coverage accounting.
"""
if result.status == "error":
return result
for entry in result.entries:
message = str(entry.get("message", ""))
is_missing_include = "Include file:" in message and "not found" in message
is_unknown_macro = "unknown macro here" in message and "configure" in message
is_cpp_header_as_c = (
"invalid C code" in message
and Path(str(entry.get("file", ""))).suffix.lower() in {".h", ".hh", ".hpp", ".hxx"}
)
if entry.get("check_id") in _CONFIGURATION_IDS or is_missing_include or is_unknown_macro or is_cpp_header_as_c:
return replace(
result, entries=[], status="error", error_kind="tool_configuration",
message=f"cppcheck requires include paths, macro definitions or language configuration: {message}",
)
return result
5 changes: 4 additions & 1 deletion desloppify/languages/cxx/detectors/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
run_tool_result,
)

from desloppify.languages.cxx.cppcheck_diagnostics import configuration_checked_result

logger = logging.getLogger(__name__)

_SOURCE_SUFFIXES = frozenset({".c", ".cc", ".cpp", ".cxx"})
Expand Down Expand Up @@ -248,7 +250,7 @@ def _normalize_kind(check_id: str, message: str) -> str | None:
):
return "command_injection"

if any(api in text for api in _UNSAFE_C_STRING_APIS):
if re.search(r"\b(?:" + "|".join(_UNSAFE_C_STRING_APIS) + r")\b", text):
return "unsafe_c_string"
if "unsafebufferhandling" in check:
return "unsafe_c_string"
Expand Down Expand Up @@ -495,6 +497,7 @@ def _run_cppcheck_invocation(scan_root: Path, files: list[str]) -> CxxToolScanRe
scan_root,
_parse_cppcheck_output,
)
result = configuration_checked_result(result)
state = _tool_result_state(result)
return CxxToolScanResult(
tool="cppcheck",
Expand Down
32 changes: 32 additions & 0 deletions desloppify/languages/cxx/tests/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,3 +611,35 @@ def _fake_run_tool_result(cmd, path, parser, **_kwargs):

assert len(result.entries) == 1
assert result.entries[0]["file"] == str(source.resolve())


def test_missing_widget_include_is_not_an_unsafe_gets_call(tmp_path):
entry = {
"file": str(tmp_path / "controller.cpp"),
"line": 11,
"severity": "information",
"check_id": "missingInclude",
"message": 'Include file: "Widgets/Input/SVirtualJoystick.h" not found.',
"source": "cppcheck",
}
assert security_mod._normalize_tool_entry(entry) is None


def test_unsafe_string_api_names_require_identifier_boundaries():
for api in security_mod._UNSAFE_C_STRING_APIS:
assert security_mod._normalize_kind("", f"Calling '{api}()' is unsafe") == "unsafe_c_string"
assert security_mod._normalize_kind(f"clang-analyzer-security.insecureAPI.{api}", "") == "unsafe_c_string"
assert security_mod._normalize_kind("", f"Include file: my_{api}_wrapper.h not found") is None


def test_cppcheck_configuration_failure_keeps_regex_security_fallback(tmp_path, monkeypatch):
source = tmp_path / "unsafe.cpp"
source.write_text("void copy(char* a, const char* b) { strcpy(a,b); }\n")
monkeypatch.setattr(security_mod.shutil, "which", lambda name: "/usr/bin/cppcheck" if name == "cppcheck" else None)
monkeypatch.setattr(security_mod, "run_tool_result", lambda *_: ToolRunResult(entries=[{
"file": str(source), "line": 1, "severity": "error", "check_id": "unknownMacro",
"message": "Unknown macro requires configuration", "source": "cppcheck",
}], status="ok", returncode=0))
result = security_mod.detect_cxx_security([str(source)], None)
assert result.coverage.status == "reduced"
assert any(e["detail"]["kind"] == "unsafe_c_string" and e["detail"]["source"] == "regex" for e in result.entries)