Skip to content

Commit 98f5504

Browse files
ar7caspergadievronclaude
authored
feature/release 2026 29 06 (#134)
Co-authored-by: gadievron <gadi@knostic.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Gadi Evron <gadievron@users.noreply.github.com>
1 parent ad17f3f commit 98f5504

75 files changed

Lines changed: 7107 additions & 289 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

libs/openant-core/context/application_context.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,26 @@ def get_type_info(self) -> dict:
147147
"""Get detailed information about this application type."""
148148
return APPLICATION_TYPE_INFO.get(self.application_type, {})
149149

150+
def suppress_local_only(self) -> bool:
151+
"""Whether to tell the analyzer to flag only REMOTE-attacker vulnerabilities.
152+
153+
The "local users have access, only flag remote" framing is correct for a
154+
CLI/library whose inputs are all operator-controlled. But a data-processing
155+
library (parser, deserializer, codec) takes UNTRUSTED INPUT DATA — and that
156+
data crossing into the code IS the attack surface, even with no network
157+
listener. ``requires_remote_trigger`` alone (False for every library) would
158+
suppress exactly those bugs. Gate on the already-captured ``trust_boundaries``:
159+
if any input source is ``untrusted``, do NOT suppress, regardless of type.
160+
"""
161+
if self.requires_remote_trigger:
162+
return False
163+
# Case-insensitive: trust_boundaries values are LLM-generated and may
164+
# deviate from the schema's lowercase 'untrusted' (e.g. 'Untrusted').
165+
return not any(
166+
str(level).lower() == "untrusted"
167+
for level in (self.trust_boundaries or {}).values()
168+
)
169+
150170

151171
# Files to check for manual override (in order of priority)
152172
MANUAL_OVERRIDE_FILES = [
@@ -488,7 +508,7 @@ def _build_type_descriptions() -> str:
488508
489509
**Guidelines:**
490510
- `application_type`: MUST be one of: web_app, cli_tool, library, agent_framework, unsupported
491-
- `requires_remote_trigger`: Set to `false` for cli_tool, library, agent_framework. Set to `true` for web_app.
511+
- `requires_remote_trigger`: Set to `true` for web_app, AND for any cli_tool/library/agent_framework that PROCESSES UNTRUSTED INPUT DATA (a parser, deserializer, codec, file/format reader, or anything where `trust_boundaries` marks an input source `untrusted` — the untrusted data crossing into the code is the attack surface even with no network listener). Set to `false` only when every input source is operator-controlled/trusted.
492512
- `confidence`: 0.0-1.0 based on how much information was available.
493513
- Be specific in `not_a_vulnerability` - these will directly prevent false positives.
494514
"""
@@ -644,7 +664,7 @@ def format_context_for_prompt(context: ApplicationContext) -> str:
644664
lines.append(f"- {item}")
645665
lines.append("")
646666

647-
if not context.requires_remote_trigger:
667+
if context.suppress_local_only():
648668
lines.append("**IMPORTANT:** This is a CLI tool/library. Users running this code have local access.")
649669
lines.append("Only flag vulnerabilities that could be exploited by a REMOTE attacker, not by local users.")
650670
lines.append("")

libs/openant-core/core/parser_adapter.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def parse_repository(
8080
name: str = None,
8181
diff_manifest: str | None = None,
8282
fresh: bool = False,
83+
library_mode: bool = False,
8384
) -> ParseResult:
8485
"""Parse a repository into an OpenAnt dataset.
8586
@@ -96,6 +97,8 @@ def parse_repository(
9697
fresh: If True, delete existing dataset.json before parsing so all
9798
units are regenerated from scratch. Only dataset.json is deleted;
9899
other artifacts in output_dir (e.g. analyzer outputs) are preserved.
100+
library_mode: If True, seed the public API surface as reachability
101+
entry points (opt-in, union-only).
99102
100103
Returns:
101104
ParseResult with paths to generated files and stats.
@@ -127,19 +130,19 @@ def parse_repository(
127130

128131
# Dispatch to the right parser
129132
if language == "python":
130-
result = _parse_python(repo_path, output_dir, processing_level, skip_tests, name)
133+
result = _parse_python(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
131134
elif language == "javascript":
132-
result = _parse_javascript(repo_path, output_dir, processing_level, skip_tests, name)
135+
result = _parse_javascript(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
133136
elif language == "go":
134-
result = _parse_go(repo_path, output_dir, processing_level, skip_tests, name)
137+
result = _parse_go(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
135138
elif language == "c":
136-
result = _parse_c(repo_path, output_dir, processing_level, skip_tests, name)
139+
result = _parse_c(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
137140
elif language == "ruby":
138-
result = _parse_ruby(repo_path, output_dir, processing_level, skip_tests, name)
141+
result = _parse_ruby(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
139142
elif language == "php":
140-
result = _parse_php(repo_path, output_dir, processing_level, skip_tests, name)
143+
result = _parse_php(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
141144
elif language == "zig":
142-
result = _parse_zig(repo_path, output_dir, processing_level, skip_tests, name)
145+
result = _parse_zig(repo_path, output_dir, processing_level, skip_tests, name, library_mode)
143146
else:
144147
raise ValueError(f"Unsupported language: {language}")
145148

@@ -207,11 +210,18 @@ def _maybe_apply_diff_filter(
207210
# Reachability filter (shared by Python path; JS/Go handle it internally)
208211
# ---------------------------------------------------------------------------
209212

213+
# library_seed_ids is now shared in utilities/agentic_enhancer/entry_point_detector.py
214+
# so every parser pipeline (not just Python) can seed the public API. It is loaded
215+
# below via the same importlib path as EntryPointDetector to dodge the heavy
216+
# utilities/__init__ imports.
217+
218+
210219
def apply_reachability_filter(
211220
dataset: dict,
212221
output_dir: str,
213222
processing_level: str,
214223
extra_entry_points: "set[str] | None" = None,
224+
library_mode: bool = False,
215225
) -> dict:
216226
"""Filter dataset units to only those reachable from entry points.
217227
@@ -254,6 +264,8 @@ def _load_module(name, filename):
254264
_epd = _load_module("entry_point_detector", "entry_point_detector.py")
255265
_ra = _load_module("reachability_analyzer", "reachability_analyzer.py")
256266
EntryPointDetector = _epd.EntryPointDetector
267+
blackout_warning = _epd.blackout_warning
268+
library_seed_ids = _epd.library_seed_ids
257269
ReachabilityAnalyzer = _ra.ReachabilityAnalyzer
258270

259271
call_graph_path = os.path.join(output_dir, "call_graph.json")
@@ -277,6 +289,11 @@ def _load_module(name, filename):
277289
entry_points = detector.detect_entry_points()
278290
if extra_entry_points:
279291
entry_points = entry_points | extra_entry_points
292+
# Library-mode (opt-in): the public API is the entry surface. Union-only —
293+
# never demotes a structurally-detected app entry point, so an app scan with
294+
# the flag on can only gain reachable units, never lose one.
295+
if library_mode:
296+
entry_points = entry_points | library_seed_ids(functions)
280297

281298
units = dataset.get("units", [])
282299
original_count = len(units)
@@ -349,6 +366,12 @@ def _load_module(name, filename):
349366
file=sys.stderr,
350367
)
351368

369+
_blackout = blackout_warning(detector.entry_point_details, original_count,
370+
len(filtered_units), library_mode=library_mode)
371+
if _blackout:
372+
dataset["metadata"]["reachability_filter"]["warning"] = _blackout
373+
print(f" [Warning] {_blackout}", file=sys.stderr)
374+
352375
# Warn about unimplemented higher-level filters
353376
if processing_level == "codeql":
354377
print(
@@ -374,7 +397,7 @@ def _load_module(name, filename):
374397
# Python parser
375398
# ---------------------------------------------------------------------------
376399

377-
def _parse_python(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
400+
def _parse_python(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
378401
"""Invoke the Python parser.
379402
380403
The Python parser has a clean `parse_repository()` function that we can
@@ -402,7 +425,8 @@ def _parse_python(repo_path: str, output_dir: str, processing_level: str, skip_t
402425

403426
# Apply reachability filter if processing_level requires it
404427
if processing_level != "all":
405-
dataset = _apply_reachability_filter(dataset, output_dir, processing_level)
428+
dataset = _apply_reachability_filter(dataset, output_dir, processing_level,
429+
library_mode=library_mode)
406430

407431
# Write outputs
408432
write_json(dataset_path, dataset)
@@ -523,7 +547,7 @@ def _file_lock(lock_path: Path):
523547
f.close()
524548

525549

526-
def _parse_javascript(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
550+
def _parse_javascript(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
527551
"""Invoke the JavaScript/TypeScript parser.
528552
529553
The JS parser is a PipelineTest class that runs Node.js subprocesses.
@@ -547,6 +571,8 @@ def _parse_javascript(repo_path: str, output_dir: str, processing_level: str, sk
547571
cmd.extend(["--name", name])
548572
if skip_tests:
549573
cmd.append("--skip-tests")
574+
if library_mode:
575+
cmd.append("--library-mode")
550576

551577
result = subprocess.run(
552578
cmd,
@@ -582,7 +608,7 @@ def _parse_javascript(repo_path: str, output_dir: str, processing_level: str, sk
582608
# Go parser
583609
# ---------------------------------------------------------------------------
584610

585-
def _parse_go(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
611+
def _parse_go(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
586612
"""Invoke the Go parser.
587613
588614
The Go parser is a PipelineTest class that calls a compiled Go binary.
@@ -603,6 +629,8 @@ def _parse_go(repo_path: str, output_dir: str, processing_level: str, skip_tests
603629
cmd.extend(["--name", name])
604630
if skip_tests:
605631
cmd.append("--skip-tests")
632+
if library_mode:
633+
cmd.append("--library-mode")
606634

607635
result = subprocess.run(
608636
cmd,
@@ -638,7 +666,7 @@ def _parse_go(repo_path: str, output_dir: str, processing_level: str, skip_tests
638666
# C/C++ parser
639667
# ---------------------------------------------------------------------------
640668

641-
def _parse_c(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
669+
def _parse_c(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
642670
"""Invoke the C/C++ parser.
643671
644672
The C parser uses tree-sitter for function extraction and call graph
@@ -661,6 +689,8 @@ def _parse_c(repo_path: str, output_dir: str, processing_level: str, skip_tests:
661689
cmd.extend(["--name", name])
662690
if skip_tests:
663691
cmd.append("--skip-tests")
692+
if library_mode:
693+
cmd.append("--library-mode")
664694

665695
result = subprocess.run(
666696
cmd,
@@ -697,7 +727,7 @@ def _parse_c(repo_path: str, output_dir: str, processing_level: str, skip_tests:
697727
# Ruby parser
698728
# ---------------------------------------------------------------------------
699729

700-
def _parse_ruby(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
730+
def _parse_ruby(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
701731
"""Invoke the Ruby parser.
702732
703733
The Ruby parser uses tree-sitter for function extraction and call graph
@@ -720,6 +750,8 @@ def _parse_ruby(repo_path: str, output_dir: str, processing_level: str, skip_tes
720750
cmd.extend(["--name", name])
721751
if skip_tests:
722752
cmd.append("--skip-tests")
753+
if library_mode:
754+
cmd.append("--library-mode")
723755

724756
result = subprocess.run(
725757
cmd,
@@ -756,7 +788,7 @@ def _parse_ruby(repo_path: str, output_dir: str, processing_level: str, skip_tes
756788
# PHP parser
757789
# ---------------------------------------------------------------------------
758790

759-
def _parse_php(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
791+
def _parse_php(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
760792
"""Invoke the PHP parser.
761793
762794
The PHP parser uses tree-sitter for function extraction and call graph
@@ -779,6 +811,8 @@ def _parse_php(repo_path: str, output_dir: str, processing_level: str, skip_test
779811
cmd.extend(["--name", name])
780812
if skip_tests:
781813
cmd.append("--skip-tests")
814+
if library_mode:
815+
cmd.append("--library-mode")
782816

783817
result = subprocess.run(
784818
cmd,
@@ -815,7 +849,7 @@ def _parse_php(repo_path: str, output_dir: str, processing_level: str, skip_test
815849
# Zig parser
816850
# ---------------------------------------------------------------------------
817851

818-
def _parse_zig(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None) -> ParseResult:
852+
def _parse_zig(repo_path: str, output_dir: str, processing_level: str, skip_tests: bool = True, name: str = None, library_mode: bool = False) -> ParseResult:
819853
"""Invoke the Zig parser.
820854
821855
The Zig parser uses tree-sitter for function extraction and call graph
@@ -838,6 +872,8 @@ def _parse_zig(repo_path: str, output_dir: str, processing_level: str, skip_test
838872
cmd.extend(["--name", name])
839873
if skip_tests:
840874
cmd.append("--skip-tests")
875+
if library_mode:
876+
cmd.append("--library-mode")
841877

842878
result = subprocess.run(
843879
cmd,

libs/openant-core/core/scanner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def scan_repository(
6262
diff_manifest: str | None = None,
6363
llm_reachability: bool = False,
6464
llm_reachability_max_code_bytes: int = 1500,
65+
library_mode: bool = False,
6566
) -> ScanResult:
6667
"""Scan a repository for vulnerabilities.
6768
@@ -171,6 +172,7 @@ def _step_label(name: str) -> str:
171172
processing_level=effective_parse_level,
172173
skip_tests=skip_tests,
173174
diff_manifest=diff_manifest,
175+
library_mode=library_mode,
174176
)
175177

176178
ctx.summary = {

libs/openant-core/openant/cli.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def cmd_scan(args):
7575
repo_url=getattr(args, "repo_url", None),
7676
commit_sha=getattr(args, "commit_sha", None),
7777
diff_manifest=getattr(args, "diff_manifest", None),
78+
library_mode=getattr(args, "library_mode", False),
7879
llm_reachability=getattr(args, "llm_reachability", False),
7980
llm_reachability_max_code_bytes=getattr(
8081
args, "llm_reachability_max_code_bytes", 1500
@@ -129,6 +130,7 @@ def cmd_parse(args):
129130
name=getattr(args, "name", None),
130131
diff_manifest=getattr(args, "diff_manifest", None),
131132
fresh=getattr(args, "fresh", False),
133+
library_mode=getattr(args, "library_mode", False),
132134
)
133135

134136
ctx.summary = {
@@ -972,7 +974,7 @@ def main():
972974
scan_p.add_argument("--output", "-o", help="Output directory (default: temp dir)")
973975
scan_p.add_argument(
974976
"--language", "-l",
975-
choices=["auto", "python", "javascript", "go", "c", "ruby", "php"],
977+
choices=["auto", "python", "javascript", "go", "c", "ruby", "php", "zig"],
976978
default="auto",
977979
help="Language (default: auto-detect)",
978980
)
@@ -995,6 +997,8 @@ def main():
995997
scan_p.add_argument("--dynamic-test", action="store_true",
996998
help="Enable Docker-isolated dynamic testing (off by default)")
997999
scan_p.add_argument("--no-skip-tests", action="store_true", help="Include test files in parsing (default: tests are skipped)")
1000+
scan_p.add_argument("--library-mode", action="store_true",
1001+
help="Seed the exported public API as entry points (for libraries with no main/route/CLI entry point)")
9981002
scan_p.add_argument("--limit", type=int, help="Max units to analyze")
9991003
scan_p.add_argument(
10001004
"--llm-config",
@@ -1047,7 +1051,7 @@ def main():
10471051
parse_p.add_argument("--output", "-o", help="Output directory (default: temp dir)")
10481052
parse_p.add_argument(
10491053
"--language", "-l",
1050-
choices=["auto", "python", "javascript", "go", "c", "ruby", "php"],
1054+
choices=["auto", "python", "javascript", "go", "c", "ruby", "php", "zig"],
10511055
default="auto",
10521056
help="Language (default: auto-detect)",
10531057
)
@@ -1058,6 +1062,8 @@ def main():
10581062
help="Processing level (default: reachable)",
10591063
)
10601064
parse_p.add_argument("--no-skip-tests", action="store_true", help="Include test files in parsing (default: tests are skipped)")
1065+
parse_p.add_argument("--library-mode", action="store_true",
1066+
help="Seed the exported public API as entry points (for libraries with no main/route/CLI entry point)")
10611067
parse_p.add_argument("--name", help="Dataset name (default: derived from repo path)")
10621068
parse_p.add_argument("--diff-manifest", help="Path to diff_manifest.json; tags units with diff_selected")
10631069
parse_p.add_argument("--fresh", action="store_true",

0 commit comments

Comments
 (0)