166 lines
5.9 KiB
Python
Executable File
166 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""POST extra artifacts + 4 relazioni per densificare Traceability + Context Explorer.
|
|
|
|
Creati in tenant t-demo-003 / unico project esistente:
|
|
1. DOC "Product Requirements.md" (DOCUMENT) --references--> REQ-103
|
|
2. COMMIT "0abc123 feat: REQ-103" (CODE_COMMIT) --generated_from--> PR#88
|
|
3. SEC "SEC-025 SQL injection input sanitization" (SECURITY_FINDING) --mitigated_by--> PR#88
|
|
4. INC "INC-77 Login timeout" (INCIDENT) --caused_by--> DEP-44 deployment
|
|
|
|
Rileva artifact IDs esistenti via LIST + ext_id exact match.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
import urllib.error
|
|
import urllib.parse
|
|
|
|
BASE = "http://localhost:8000/api/v1"
|
|
H = {
|
|
"X-ASF-Tenant-Id": "t-demo-003",
|
|
"X-ASF-User-Id": "u-mario",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
LOOKUP = {
|
|
"REQ": "REQ-103",
|
|
"PR": "PR#88",
|
|
"DEP": "DEP-44",
|
|
}
|
|
|
|
|
|
def req(method: str, path: str, body=None) -> tuple[int, object]:
|
|
data = None
|
|
if body is not None:
|
|
data = json.dumps(body).encode()
|
|
r = urllib.request.Request(BASE + path, data=data, method=method, headers=H)
|
|
try:
|
|
with urllib.request.urlopen(r, timeout=10) as resp:
|
|
text = resp.read().decode()
|
|
return resp.status, json.loads(text) if text else None
|
|
except urllib.error.HTTPError as e:
|
|
body = e.read().decode()
|
|
print(f" !! {method} {path} -> {e.code}: {body[:300]}", file=sys.stderr)
|
|
return e.code, body
|
|
|
|
|
|
def list_artifacts() -> list[dict]:
|
|
s, d = req("GET", "/artifacts?limit=500")
|
|
assert s == 200 and isinstance(d, list), f"list artifacts failed: {s}"
|
|
return d
|
|
|
|
|
|
def find_project_id(arts: list[dict]) -> str:
|
|
"""Unico project del tenant: take artifact project_id non-None first."""
|
|
for a in arts:
|
|
if a.get("project_id"):
|
|
return a["project_id"]
|
|
s, pl = req("GET", "/projects?limit=50")
|
|
assert s == 200 and isinstance(pl, list) and pl
|
|
return pl[0]["id"]
|
|
|
|
|
|
def create_artifact(payload: dict) -> dict:
|
|
s, d = req("POST", "/artifacts", payload)
|
|
if s != 201 or not isinstance(d, dict):
|
|
raise RuntimeError(f"CREATE {payload.get('type')} {payload.get('external_id')} HTTP {s}")
|
|
return d
|
|
|
|
|
|
def create_rel(src: str, tgt: str, rel_type: str) -> int:
|
|
body = {
|
|
"source_artifact_id": src,
|
|
"target_artifact_id": tgt,
|
|
"relationship_type": rel_type,
|
|
}
|
|
s, _ = req("POST", "/relationships", body)
|
|
return s
|
|
|
|
|
|
def main() -> int:
|
|
arts = list_artifacts()
|
|
existing: dict[str, str] = {}
|
|
for k, ext in LOOKUP.items():
|
|
match = next((a for a in arts if a.get("external_id") == ext), None)
|
|
if match is None:
|
|
print(f" !! missing {ext}, aborting.")
|
|
return 2
|
|
existing[k] = match["id"]
|
|
print(f" {k:3s} {ext!r:12s} -> {match['id'][:10]}…")
|
|
|
|
pid = find_project_id(arts)
|
|
print(f" project id = {pid[:10]}…")
|
|
|
|
print("\n== Creating 4 extra artifacts ==")
|
|
extra = [
|
|
{
|
|
"project_id": pid,
|
|
"type": "document",
|
|
"external_id": "DOC-PRD-2026",
|
|
"title": "Product Requirements v2.3",
|
|
"status": "active",
|
|
"content": "# Product Requirements 2.3\n\nLiving PRD covering authentication, billing and reporting.\n\n## Goals\nProvide clear tenancy and billing.\n",
|
|
"metadata": {"format": "markdown", "page_count": 18, "owner": "pm"},
|
|
},
|
|
{
|
|
"project_id": pid,
|
|
"type": "commit",
|
|
"external_id": "COMMIT-0abc123",
|
|
"title": "feat(auth): implement REQ-103 SSO flow",
|
|
"content": "commit 0abc123df\nAuthor: mario\nDate: today\n\nAdds Okta SAML callback + session refresh route.\n",
|
|
"metadata": {"sha": "0abc123df", "author": "mario@asf.test", "repo": "asf/mono", "branch": "main"},
|
|
},
|
|
{
|
|
"project_id": pid,
|
|
"type": "security_finding",
|
|
"external_id": "SEC-025",
|
|
"title": "SQL injection risk in profile search (low confidence)",
|
|
"status": "open",
|
|
"content": "Input param q passed directly to $regex without sanitization. Scanner: semgrep. Confidence 82%.",
|
|
"metadata": {"cwe": "CWE-89", "severity": "high", "scanner": "semgrep", "confidence_pct": 82},
|
|
},
|
|
{
|
|
"project_id": pid,
|
|
"type": "incident",
|
|
"external_id": "INC-77",
|
|
"title": "Login timeout spike 05:00-05:14 UTC",
|
|
"status": "mitigated",
|
|
"content": "Session refresh endpoint returned 5xx for 4% of users. MTTR 14 minutes. 27 customers impacted.",
|
|
"metadata": {"severity": "sev2", "mttr_min": 14, "customers_impacted": 27},
|
|
},
|
|
]
|
|
ids: list[str] = []
|
|
for pl in extra:
|
|
d = create_artifact(pl)
|
|
print(f" [201] {pl['type']:15s} {pl['external_id']:18s} id={d['id'][:8]}…")
|
|
ids.append(d["id"])
|
|
|
|
doc_id, commit_id, sec_id, inc_id = ids
|
|
print("\n== Creating 4 extra relationships ==")
|
|
edges = [
|
|
(doc_id, existing["REQ"], "documented_by", "DOC --documented_by--> REQ"),
|
|
(commit_id, existing["PR"], "generated_from", "COMMIT --generated_from--> PR"),
|
|
(sec_id, existing["PR"], "related_to", "SEC --related_to--> PR"),
|
|
(inc_id, existing["DEP"], "caused_by", "INC --caused_by--> DEP"),
|
|
]
|
|
all_ok = True
|
|
for src, tgt, rt, label in edges:
|
|
s = create_rel(src, tgt, rt)
|
|
chk = "OK" if s == 201 else "FAIL"
|
|
print(f" [{s:3d}] {chk:4s} {label}")
|
|
if s != 201:
|
|
all_ok = False
|
|
|
|
time.sleep(0.3)
|
|
s, g = req("GET", f"/traceability/graph?project_id={pid}")
|
|
if s == 200 and isinstance(g, dict):
|
|
print(f"\n== Traceability graph now: nodes={len(g.get('nodes', []))} edges={len(g.get('edges', []))} ==")
|
|
return 0 if all_ok else 5
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|