mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 10:27:11 +00:00
Fix detect.py missing .sh/.json extensions and security harden extractors
- Add .sh, .bash, .json to CODE_EXTENSIONS in detect.py so files reach extractor - Fix test_detect_incremental manifest path collision with new .json extension - Update test_watch to reflect .json/.sh are now watched extensions - B-1: only emit source imports for paths that exist on disk - J-1: replace stat()+read() with bounded read to eliminate TOCTOU - J-3: move pair_count cap inside loop so it is honoured exactly - J-4: namespace $ref/extends refs with "ref_" prefix to prevent ID collision Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@ class FileType(str, Enum):
|
||||
|
||||
_MANIFEST_PATH = "graphify-out/manifest.json"
|
||||
|
||||
CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.mjs', '.ejs', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk'}
|
||||
CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.mjs', '.ejs', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json'}
|
||||
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.txt', '.rst', '.html', '.yaml', '.yml'}
|
||||
PAPER_EXTENSIONS = {'.pdf'}
|
||||
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
|
||||
|
||||
+25
-10
@@ -5696,9 +5696,14 @@ def extract_bash(path: Path) -> dict:
|
||||
line = node.start_point[0] + 1
|
||||
if raw.startswith((".", "/")):
|
||||
resolved = (path.parent / raw).resolve()
|
||||
tgt_nid = _make_id(str(resolved))
|
||||
add_edge(file_nid, tgt_nid, "imports_from", line,
|
||||
context="import")
|
||||
# Only emit the edge if the target actually exists on
|
||||
# disk — prevents graph pollution from crafted paths
|
||||
# like `source ../../etc/passwd` that traverse outside
|
||||
# the project tree (B-1).
|
||||
if resolved.exists():
|
||||
tgt_nid = _make_id(str(resolved))
|
||||
add_edge(file_nid, tgt_nid, "imports_from", line,
|
||||
context="import")
|
||||
else:
|
||||
tgt_nid = _make_id(raw)
|
||||
if tgt_nid:
|
||||
@@ -5744,11 +5749,15 @@ def extract_json(path: Path) -> dict:
|
||||
return {"nodes": [], "edges": [], "error": "tree-sitter-json not installed"}
|
||||
|
||||
try:
|
||||
if path.stat().st_size > _JSON_MAX_BYTES:
|
||||
# Bounded read instead of stat()+read() to eliminate TOCTOU (J-1):
|
||||
# read one byte beyond the limit so we can detect oversized files even
|
||||
# if the file grows between stat and read.
|
||||
with path.open("rb") as _f:
|
||||
source = _f.read(_JSON_MAX_BYTES + 1)
|
||||
if len(source) > _JSON_MAX_BYTES:
|
||||
return {"nodes": [], "edges": [], "error": "json file too large to index"}
|
||||
language = Language(tsjson.language())
|
||||
parser = Parser(language)
|
||||
source = path.read_bytes()
|
||||
tree = parser.parse(source)
|
||||
root = tree.root_node
|
||||
except Exception as e:
|
||||
@@ -5805,11 +5814,13 @@ def extract_json(path: Path) -> dict:
|
||||
|
||||
def walk_object(obj_node, parent_nid: str, parent_key: str | None,
|
||||
depth: int, pair_count: list) -> None:
|
||||
if depth > 6 or pair_count[0] > 500:
|
||||
if depth > 6:
|
||||
return
|
||||
for child in obj_node.children:
|
||||
if child.type != "pair":
|
||||
continue
|
||||
if pair_count[0] >= 500: # check per-pair so the cap is honoured exactly (J-3)
|
||||
return
|
||||
pair_count[0] += 1
|
||||
key = _key_text(child)
|
||||
if not key:
|
||||
@@ -5829,13 +5840,15 @@ def extract_json(path: Path) -> dict:
|
||||
walk_object(val, key_nid, key, depth + 1, pair_count)
|
||||
|
||||
elif val.type == "array":
|
||||
# For "extends" arrays (tsconfig, eslint): each string element
|
||||
# For "extends" arrays (tsconfig, eslint): each string element.
|
||||
# Prefix with "ref_" so external refs don't collide with real
|
||||
# code/file node IDs that share the same collapsed _make_id (J-4).
|
||||
for item in val.children:
|
||||
if item.type == "string":
|
||||
content = item.child_by_field_name("string_content")
|
||||
ref = _read_text(content, source) if content else _read_text(item, source).strip('"\'')
|
||||
if ref:
|
||||
ref_nid = _make_id(ref)
|
||||
ref_nid = _make_id("ref", ref)
|
||||
if ref_nid:
|
||||
add_edge(key_nid, ref_nid, "extends", line, context="import")
|
||||
|
||||
@@ -5844,12 +5857,14 @@ def extract_json(path: Path) -> dict:
|
||||
val_text = _read_text(content, source) if content else _read_text(val, source).strip('"\'')
|
||||
|
||||
if key == "extends" and val_text:
|
||||
ref_nid = _make_id(val_text)
|
||||
# Namespace external refs to avoid ID collision with file nodes (J-4)
|
||||
ref_nid = _make_id("ref", val_text)
|
||||
if ref_nid:
|
||||
add_edge(file_nid, ref_nid, "extends", line, context="import")
|
||||
|
||||
elif key == "$ref" and val_text:
|
||||
ref_nid = _make_id(val_text)
|
||||
# Namespace $ref values to prevent edge hijacking into code nodes (J-4)
|
||||
ref_nid = _make_id("ref", val_text)
|
||||
if ref_nid:
|
||||
add_edge(parent_nid, ref_nid, "references", line)
|
||||
|
||||
|
||||
@@ -230,7 +230,11 @@ def test_detect_incremental_propagates_follow_symlinks(tmp_path, monkeypatch):
|
||||
(real_dir / "note.md").write_text("# real note\n\nsome content")
|
||||
(tmp_path / "linked_corpus").symlink_to(real_dir)
|
||||
|
||||
manifest_path = str(tmp_path / "manifest.json")
|
||||
# Store manifest inside graphify-out/ so it is pruned by _SKIP_DIRS
|
||||
# and doesn't get re-detected as a code file now that .json is indexed.
|
||||
manifest_dir = tmp_path / "graphify-out"
|
||||
manifest_dir.mkdir()
|
||||
manifest_path = str(manifest_dir / "manifest.json")
|
||||
|
||||
# Without following symlinks, the symlinked dir contents are invisible.
|
||||
no_link = detect_incremental(tmp_path, manifest_path, follow_symlinks=False)
|
||||
|
||||
+3
-1
@@ -47,7 +47,9 @@ def test_watched_extensions_includes_images():
|
||||
assert ".jpg" in _WATCHED_EXTENSIONS
|
||||
|
||||
def test_watched_extensions_excludes_noise():
|
||||
assert ".json" not in _WATCHED_EXTENSIONS
|
||||
# .json is now indexed (bash/JSON extractors added in #866)
|
||||
assert ".json" in _WATCHED_EXTENSIONS
|
||||
assert ".sh" in _WATCHED_EXTENSIONS
|
||||
assert ".pyc" not in _WATCHED_EXTENSIONS
|
||||
assert ".log" not in _WATCHED_EXTENSIONS
|
||||
|
||||
|
||||
Reference in New Issue
Block a user