Fix 8 bugs: SSRF in tweet fetch, Cypher injection, XSS in community labels, graph_diff edge ordering, cache key collision, hook uninstall correctness, watch mode set, rebuild word count
This commit is contained in:
+1
-1
@@ -472,7 +472,7 @@ def graph_diff(G_old: nx.Graph, G_new: nx.Graph) -> dict:
|
||||
]
|
||||
|
||||
def edge_key(G: nx.Graph, u: str, v: str, data: dict) -> tuple:
|
||||
return (u, v, data.get("relation", ""))
|
||||
return (min(u, v), max(u, v), data.get("relation", ""))
|
||||
|
||||
old_edge_keys = {
|
||||
edge_key(G_old, u, v, d)
|
||||
|
||||
+7
-2
@@ -8,8 +8,13 @@ from pathlib import Path
|
||||
|
||||
|
||||
def file_hash(path: Path) -> str:
|
||||
"""SHA256 of file contents, hex digest."""
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
"""SHA256 of file contents + resolved path. Prevents cache collisions on identical content."""
|
||||
p = Path(path)
|
||||
h = hashlib.sha256()
|
||||
h.update(p.read_bytes())
|
||||
h.update(b"\x00")
|
||||
h.update(str(p.resolve()).encode())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def cache_dir(root: Path = Path(".")) -> Path:
|
||||
|
||||
+15
-7
@@ -271,18 +271,26 @@ def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str) ->
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
def _cypher_escape(s: str) -> str:
|
||||
"""Escape a string for safe embedding in a Cypher single-quoted literal."""
|
||||
return s.replace("\\", "\\\\").replace("'", "\\'")
|
||||
|
||||
|
||||
def to_cypher(G: nx.Graph, output_path: str) -> None:
|
||||
lines = ["// Neo4j Cypher import - generated by /graphify", ""]
|
||||
for node_id, data in G.nodes(data=True):
|
||||
label = data.get("label", node_id).replace("'", "\\'")
|
||||
ftype = data.get("file_type", "unknown").capitalize()
|
||||
lines.append(f"MERGE (n:{ftype} {{id: '{node_id}', label: '{label}'}});")
|
||||
label = _cypher_escape(data.get("label", node_id))
|
||||
node_id_esc = _cypher_escape(node_id)
|
||||
ftype = re.sub(r"[^A-Za-z0-9_]", "", data.get("file_type", "unknown").capitalize()) or "Entity"
|
||||
lines.append(f"MERGE (n:{ftype} {{id: '{node_id_esc}', label: '{label}'}});")
|
||||
lines.append("")
|
||||
for u, v, data in G.edges(data=True):
|
||||
rel = data.get("relation", "RELATES_TO").upper().replace(" ", "_").replace("-", "_")
|
||||
conf = data.get("confidence", "EXTRACTED")
|
||||
rel = re.sub(r"[^A-Za-z0-9_]", "_", data.get("relation", "RELATES_TO").upper())
|
||||
conf = _cypher_escape(data.get("confidence", "EXTRACTED"))
|
||||
u_esc = _cypher_escape(u)
|
||||
v_esc = _cypher_escape(v)
|
||||
lines.append(
|
||||
f"MATCH (a {{id: '{u}'}}), (b {{id: '{v}'}}) "
|
||||
f"MATCH (a {{id: '{u_esc}'}}), (b {{id: '{v_esc}'}}) "
|
||||
f"MERGE (a)-[:{rel} {{confidence: '{conf}'}}]->(b);"
|
||||
)
|
||||
with open(output_path, "w") as f:
|
||||
@@ -329,7 +337,7 @@ def to_html(
|
||||
"font": {"size": font_size, "color": "#ffffff"},
|
||||
"title": f"{label}",
|
||||
"community": cid,
|
||||
"community_name": (community_labels or {}).get(cid, f"Community {cid}"),
|
||||
"community_name": sanitize_label((community_labels or {}).get(cid, f"Community {cid}")),
|
||||
"source_file": sanitize_label(data.get("source_file", "")),
|
||||
"file_type": data.get("file_type", ""),
|
||||
"degree": deg,
|
||||
|
||||
+22
-15
@@ -1,13 +1,15 @@
|
||||
# git hook integration - install/uninstall graphify post-commit and post-checkout hooks
|
||||
from __future__ import annotations
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
_HOOK_MARKER = "# graphify-hook"
|
||||
_CHECKOUT_MARKER = "# graphify-checkout-hook"
|
||||
_HOOK_MARKER = "# graphify-hook-start"
|
||||
_HOOK_MARKER_END = "# graphify-hook-end"
|
||||
_CHECKOUT_MARKER = "# graphify-checkout-hook-start"
|
||||
_CHECKOUT_MARKER_END = "# graphify-checkout-hook-end"
|
||||
|
||||
_HOOK_SCRIPT = """\
|
||||
#!/bin/bash
|
||||
# graphify-hook
|
||||
# graphify-hook-start
|
||||
# Auto-rebuilds the knowledge graph after each commit (code files only, no LLM needed).
|
||||
# Installed by: graphify hook install
|
||||
|
||||
@@ -42,12 +44,12 @@ except Exception as exc:
|
||||
print(f'[graphify hook] Rebuild failed: {exc}')
|
||||
sys.exit(1)
|
||||
"
|
||||
# graphify-hook-end
|
||||
"""
|
||||
|
||||
|
||||
_CHECKOUT_SCRIPT = """\
|
||||
#!/bin/bash
|
||||
# graphify-checkout-hook
|
||||
# graphify-checkout-hook-start
|
||||
# Auto-rebuilds the knowledge graph (code only) when switching branches.
|
||||
# Installed by: graphify hook install
|
||||
|
||||
@@ -76,6 +78,7 @@ except Exception as exc:
|
||||
print(f'[graphify] Rebuild failed: {exc}')
|
||||
sys.exit(1)
|
||||
"
|
||||
# graphify-checkout-hook-end
|
||||
"""
|
||||
|
||||
|
||||
@@ -97,25 +100,29 @@ def _install_hook(hooks_dir: Path, name: str, script: str, marker: str) -> str:
|
||||
return f"already installed at {hook_path}"
|
||||
hook_path.write_text(content.rstrip() + "\n\n" + script)
|
||||
return f"appended to existing {name} hook at {hook_path}"
|
||||
hook_path.write_text(script)
|
||||
hook_path.write_text("#!/bin/bash\n" + script)
|
||||
hook_path.chmod(0o755)
|
||||
return f"installed at {hook_path}"
|
||||
|
||||
|
||||
def _uninstall_hook(hooks_dir: Path, name: str, marker: str) -> str:
|
||||
"""Remove graphify section from a git hook."""
|
||||
def _uninstall_hook(hooks_dir: Path, name: str, marker: str, marker_end: str) -> str:
|
||||
"""Remove graphify section from a git hook using start/end markers."""
|
||||
hook_path = hooks_dir / name
|
||||
if not hook_path.exists():
|
||||
return f"no {name} hook found - nothing to remove."
|
||||
content = hook_path.read_text()
|
||||
if marker not in content:
|
||||
return f"graphify hook not found in {name} - nothing to remove."
|
||||
before = content.split(marker)[0].rstrip()
|
||||
non_empty = [l for l in before.splitlines() if l.strip() and not l.startswith("#!")]
|
||||
if not non_empty:
|
||||
new_content = re.sub(
|
||||
rf"{re.escape(marker)}.*?{re.escape(marker_end)}\n?",
|
||||
"",
|
||||
content,
|
||||
flags=re.DOTALL,
|
||||
).strip()
|
||||
if not new_content or new_content == "#!/bin/bash":
|
||||
hook_path.unlink()
|
||||
return f"removed {name} hook at {hook_path}"
|
||||
hook_path.write_text(before + "\n")
|
||||
hook_path.write_text(new_content + "\n")
|
||||
return f"graphify removed from {name} at {hook_path} (other hook content preserved)"
|
||||
|
||||
|
||||
@@ -141,8 +148,8 @@ def uninstall(path: Path = Path(".")) -> str:
|
||||
raise RuntimeError(f"No git repository found at or above {path.resolve()}")
|
||||
|
||||
hooks_dir = root / ".git" / "hooks"
|
||||
commit_msg = _uninstall_hook(hooks_dir, "post-commit", _HOOK_MARKER)
|
||||
checkout_msg = _uninstall_hook(hooks_dir, "post-checkout", _CHECKOUT_MARKER)
|
||||
commit_msg = _uninstall_hook(hooks_dir, "post-commit", _HOOK_MARKER, _HOOK_MARKER_END)
|
||||
checkout_msg = _uninstall_hook(hooks_dir, "post-checkout", _CHECKOUT_MARKER, _CHECKOUT_MARKER_END)
|
||||
|
||||
return f"post-commit: {commit_msg}\npost-checkout: {checkout_msg}"
|
||||
|
||||
|
||||
+1
-3
@@ -67,9 +67,7 @@ def _fetch_tweet(url: str, author: str | None, contributor: str | None) -> tuple
|
||||
oembed_url = url.replace("x.com", "twitter.com")
|
||||
oembed_api = f"https://publish.twitter.com/oembed?url={urllib.parse.quote(oembed_url)}&omit_script=true"
|
||||
try:
|
||||
req = urllib.request.Request(oembed_api, headers={"User-Agent": "graphify/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = json.loads(resp.read())
|
||||
data = json.loads(safe_fetch_text(oembed_api))
|
||||
tweet_text = re.sub(r"<[^>]+>", "", data.get("html", "")).strip()
|
||||
tweet_author = data.get("author_name", "unknown")
|
||||
except Exception:
|
||||
|
||||
+3
-4
@@ -50,7 +50,7 @@ def _rebuild_code(watch_path: Path) -> bool:
|
||||
detection = {
|
||||
"files": {"code": [str(f) for f in code_files], "document": [], "paper": [], "image": []},
|
||||
"total_files": len(code_files),
|
||||
"total_words": sum(len(f.read_text(errors="ignore").split()) for f in code_files),
|
||||
"total_words": 0, # not needed during watch rebuild
|
||||
}
|
||||
|
||||
G = build_from_json(result)
|
||||
@@ -118,7 +118,7 @@ def watch(watch_path: Path, debounce: float = 3.0) -> None:
|
||||
|
||||
last_trigger: float = 0.0
|
||||
pending: bool = False
|
||||
changed: list[Path] = []
|
||||
changed: set[Path] = set()
|
||||
|
||||
class Handler(FileSystemEventHandler):
|
||||
def on_any_event(self, event):
|
||||
@@ -134,8 +134,7 @@ def watch(watch_path: Path, debounce: float = 3.0) -> None:
|
||||
return
|
||||
last_trigger = time.monotonic()
|
||||
pending = True
|
||||
if path not in changed:
|
||||
changed.append(path)
|
||||
changed.add(path)
|
||||
|
||||
handler = Handler()
|
||||
observer = Observer()
|
||||
|
||||
Reference in New Issue
Block a user