Skip to content

Commit 551f2cb

Browse files
committed
fix delete_relationship() method
1 parent 8d495d6 commit 551f2cb

File tree

10 files changed

+390
-27
lines changed

10 files changed

+390
-27
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,12 +292,18 @@ j1.update_relationship(
292292
##### Delete a relationship
293293

294294
```python
295-
# Delete by relationship ID
296-
j1.delete_relationship(relationship_id='<id-of-relationship-to-delete>')
295+
# Delete a relationship (requires relationship ID, source entity ID, and target entity ID)
296+
j1.delete_relationship(
297+
relationship_id='<id-of-relationship-to-delete>',
298+
from_entity_id='<id-of-source-entity>',
299+
to_entity_id='<id-of-destination-entity>'
300+
)
297301

298302
# Delete with timestamp
299303
j1.delete_relationship(
300304
relationship_id='<id-of-relationship-to-delete>',
305+
from_entity_id='<id-of-source-entity>',
306+
to_entity_id='<id-of-destination-entity>',
301307
timestamp=int(time.time()) * 1000
302308
)
303309
```

examples/03_relationship_management.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,20 +175,26 @@ def update_relationship_examples(j1, relationship_id, from_entity_id, to_entity_
175175
)
176176
print(f"Updated with custom timestamp\n")
177177

178-
def delete_relationship_examples(j1, relationship_id):
178+
def delete_relationship_examples(j1, relationship_id, from_entity_id, to_entity_id):
179179
"""Demonstrate relationship deletion."""
180180

181181
print("=== Relationship Deletion Examples ===\n")
182182

183183
# 1. Basic deletion
184184
print("1. Deleting a relationship:")
185-
delete_result = j1.delete_relationship(relationship_id=relationship_id)
185+
delete_result = j1.delete_relationship(
186+
relationship_id=relationship_id,
187+
from_entity_id=from_entity_id,
188+
to_entity_id=to_entity_id
189+
)
186190
print(f"Deleted relationship: {delete_result['relationship']['_id']}\n")
187191

188192
# 2. Deletion with timestamp
189193
print("2. Deleting with specific timestamp:")
190194
j1.delete_relationship(
191195
relationship_id=relationship_id,
196+
from_entity_id=from_entity_id,
197+
to_entity_id=to_entity_id,
192198
timestamp=int(time.time()) * 1000
193199
)
194200
print(f"Deleted with timestamp\n")
@@ -234,7 +240,11 @@ def relationship_lifecycle_example(j1, from_entity_id, to_entity_id):
234240

235241
# 4. Delete relationship
236242
print("4. Deleting relationship:")
237-
j1.delete_relationship(relationship_id=relationship_id)
243+
j1.delete_relationship(
244+
relationship_id=relationship_id,
245+
from_entity_id=from_entity_id,
246+
to_entity_id=to_entity_id
247+
)
238248
print("Deleted successfully")
239249

240250
# 5. Verify deletion
@@ -281,14 +291,22 @@ def network_relationship_example(j1):
281291
'bandwidth': '100Mbps'
282292
}
283293
)
284-
relationships.append(relationship['relationship']['_id'])
294+
relationships.append({
295+
'id': relationship['relationship']['_id'],
296+
'from': entities[i],
297+
'to': entities[i+1]
298+
})
285299
print(f"Created connection {i}: {relationship['relationship']['_id']}")
286300

287301
print(f"Created {len(entities)} nodes with {len(relationships)} connections")
288302

289303
# Clean up
290-
for relationship_id in relationships:
291-
j1.delete_relationship(relationship_id=relationship_id)
304+
for rel in relationships:
305+
j1.delete_relationship(
306+
relationship_id=rel['id'],
307+
from_entity_id=rel['from'],
308+
to_entity_id=rel['to']
309+
)
292310
for entity_id in entities:
293311
j1.delete_entity(entity_id=entity_id)
294312

@@ -356,7 +374,11 @@ def access_control_relationship_example(j1):
356374
print("Updated access level to write")
357375

358376
# Clean up
359-
j1.delete_relationship(relationship_id=access_relationship['relationship']['_id'])
377+
j1.delete_relationship(
378+
relationship_id=access_relationship['relationship']['_id'],
379+
from_entity_id=user_entity['entity']['_id'],
380+
to_entity_id=resource_entity['entity']['_id']
381+
)
360382
j1.delete_entity(entity_id=user_entity['entity']['_id'])
361383
j1.delete_entity(entity_id=resource_entity['entity']['_id'])
362384

@@ -397,7 +419,11 @@ def main():
397419
relationships_to_clean = [basic_rel, props_rel, complex_rel]
398420
for rel in relationships_to_clean:
399421
try:
400-
j1.delete_relationship(relationship_id=rel['relationship']['_id'])
422+
j1.delete_relationship(
423+
relationship_id=rel['relationship']['_id'],
424+
from_entity_id=from_entity_id,
425+
to_entity_id=to_entity_id
426+
)
401427
print(f"Cleaned up relationship: {rel['relationship']['_id']}")
402428
except Exception:
403429
# Relationship may already be deleted or not exist

examples/06_advanced_operations.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,11 @@ def bulk_operations_examples(j1):
112112
for rel_data in relationships_to_create:
113113
try:
114114
relationship = j1.create_relationship(**rel_data)
115-
created_relationships.append(relationship['relationship']['_id'])
115+
created_relationships.append({
116+
'id': relationship['relationship']['_id'],
117+
'from': rel_data['from_entity_id'],
118+
'to': rel_data['to_entity_id']
119+
})
116120
print(f"Created relationship: {relationship['relationship']['_id']}")
117121
except Exception as e:
118122
print(f"Error creating relationship: {e}")
@@ -137,29 +141,35 @@ def bulk_operations_examples(j1):
137141

138142
# 4. Bulk relationship updates
139143
print("4. Bulk relationship updates:")
140-
for rel_id in created_relationships:
144+
for rel in created_relationships:
141145
try:
142146
j1.update_relationship(
143-
relationship_id=rel_id,
147+
relationship_id=rel['id'],
148+
from_entity_id=rel['from'],
149+
to_entity_id=rel['to'],
144150
properties={
145151
"lastUpdated": int(time.time()) * 1000,
146152
"tag.BulkUpdated": "true"
147153
}
148154
)
149-
print(f"Updated relationship: {rel_id}")
155+
print(f"Updated relationship: {rel['id']}")
150156
except Exception as e:
151-
print(f"Error updating relationship {rel_id}: {e}")
157+
print(f"Error updating relationship {rel['id']}: {e}")
152158
print()
153159

154160
# 5. Bulk deletion
155161
print("5. Bulk deletion:")
156162
# Delete relationships first
157-
for rel_id in created_relationships:
163+
for rel in created_relationships:
158164
try:
159-
j1.delete_relationship(relationship_id=rel_id)
160-
print(f"Deleted relationship: {rel_id}")
165+
j1.delete_relationship(
166+
relationship_id=rel['id'],
167+
from_entity_id=rel['from'],
168+
to_entity_id=rel['to']
169+
)
170+
print(f"Deleted relationship: {rel['id']}")
161171
except Exception as e:
162-
print(f"Error deleting relationship {rel_id}: {e}")
172+
print(f"Error deleting relationship {rel['id']}: {e}")
163173

164174
# Then delete entities
165175
for entity_id in created_entities:

examples/examples.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@
8585
print(create_relationship_r)
8686

8787
# delete_relationship
88-
delete_relationship_r = j1.delete_relationship(relationship_id=create_relationship_r['relationship']['_id'])
88+
delete_relationship_r = j1.delete_relationship(
89+
relationship_id=create_relationship_r['relationship']['_id'],
90+
from_entity_id=create_r['entity']['_id'],
91+
to_entity_id=create_r_2['entity']['_id']
92+
)
8993
print("delete_relationship()")
9094
print(delete_relationship_r)
9195

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Find critical Snyk findings linked to resolved issues where the issue
4+
was resolved (updatedOn) within 15 days of being opened (createdOn).
5+
6+
J1QL cannot perform property-to-property arithmetic in WHERE clauses,
7+
so we retrieve both timestamps and filter in Python.
8+
9+
Usage:
10+
export JUPITERONE_ACCOUNT_ID="<your-account-id>"
11+
export JUPITERONE_API_TOKEN="<your-api-token>"
12+
python snyk_findings_resolved_within_15_days.py [--csv output.csv]
13+
"""
14+
15+
import os
16+
import sys
17+
import csv
18+
import argparse
19+
from datetime import datetime, timezone
20+
21+
from jupiterone import JupiterOneClient
22+
23+
FIFTEEN_DAYS_MS = 15 * 24 * 60 * 60 * 1000
24+
25+
J1QL_QUERY = """\
26+
FIND snyk_finding WITH severity = 'critical' AS finding
27+
THAT RELATES TO snyk_issue WITH status = 'resolved' AS issue
28+
RETURN
29+
finding.displayName AS findingName,
30+
finding._key AS findingKey,
31+
finding.severity AS severity,
32+
issue.displayName AS issueName,
33+
issue._key AS issueKey,
34+
issue.status AS issueStatus,
35+
issue.createdOn AS createdOn,
36+
issue.updatedOn AS updatedOn
37+
LIMIT 250\
38+
"""
39+
40+
41+
def ms_to_iso(epoch_ms):
42+
"""Convert epoch milliseconds to a human-readable ISO-8601 string."""
43+
if epoch_ms is None:
44+
return "N/A"
45+
try:
46+
return datetime.fromtimestamp(epoch_ms / 1000, tz=timezone.utc).strftime(
47+
"%Y-%m-%d %H:%M:%S UTC"
48+
)
49+
except (TypeError, ValueError, OSError):
50+
return str(epoch_ms)
51+
52+
53+
def main():
54+
parser = argparse.ArgumentParser(
55+
description="Find critical Snyk findings resolved within 15 days."
56+
)
57+
parser.add_argument(
58+
"--csv",
59+
metavar="FILE",
60+
help="Write results to a CSV file instead of stdout.",
61+
)
62+
args = parser.parse_args()
63+
64+
account = os.getenv("JUPITERONE_ACCOUNT_ID")
65+
token = os.getenv("JUPITERONE_API_TOKEN")
66+
if not account or not token:
67+
sys.exit(
68+
"Error: JUPITERONE_ACCOUNT_ID and JUPITERONE_API_TOKEN "
69+
"environment variables are required."
70+
)
71+
72+
j1 = JupiterOneClient(
73+
account=account,
74+
token=token,
75+
url=os.getenv("JUPITERONE_URL", "https://graphql.us.jupiterone.io"),
76+
sync_url=os.getenv("JUPITERONE_SYNC_URL", "https://api.us.jupiterone.io"),
77+
)
78+
79+
print(f"Executing J1QL query ...\n{J1QL_QUERY}\n")
80+
result = j1.query_v1(query=J1QL_QUERY)
81+
rows = result.get("data", [])
82+
print(f"Total rows returned: {len(rows)}")
83+
84+
filtered = []
85+
skipped_missing_dates = 0
86+
87+
for row in rows:
88+
props = row.get("properties", row)
89+
created_on = props.get("createdOn")
90+
updated_on = props.get("updatedOn")
91+
92+
if created_on is None or updated_on is None:
93+
skipped_missing_dates += 1
94+
continue
95+
96+
delta_ms = updated_on - created_on
97+
if delta_ms <= FIFTEEN_DAYS_MS:
98+
filtered.append(
99+
{
100+
"findingName": props.get("findingName", ""),
101+
"findingKey": props.get("findingKey", ""),
102+
"severity": props.get("severity", ""),
103+
"issueName": props.get("issueName", ""),
104+
"issueKey": props.get("issueKey", ""),
105+
"issueStatus": props.get("issueStatus", ""),
106+
"createdOn": created_on,
107+
"updatedOn": updated_on,
108+
"createdOnHuman": ms_to_iso(created_on),
109+
"updatedOnHuman": ms_to_iso(updated_on),
110+
"daysToResolve": round(delta_ms / (24 * 60 * 60 * 1000), 2),
111+
}
112+
)
113+
114+
print(f"Rows matching <=15-day window: {len(filtered)}")
115+
if skipped_missing_dates:
116+
print(f"Rows skipped (missing createdOn/updatedOn): {skipped_missing_dates}")
117+
118+
if not filtered:
119+
print("No matching results.")
120+
return
121+
122+
if args.csv:
123+
fieldnames = list(filtered[0].keys())
124+
with open(args.csv, "w", newline="") as f:
125+
writer = csv.DictWriter(f, fieldnames=fieldnames)
126+
writer.writeheader()
127+
writer.writerows(filtered)
128+
print(f"\nResults written to {args.csv}")
129+
else:
130+
print(f"\n{'Finding':<40} {'Issue':<40} {'Created':<24} {'Updated':<24} {'Days':>6}")
131+
print("-" * 138)
132+
for r in filtered:
133+
print(
134+
f"{r['findingName']:<40} "
135+
f"{r['issueName']:<40} "
136+
f"{r['createdOnHuman']:<24} "
137+
f"{r['updatedOnHuman']:<24} "
138+
f"{r['daysToResolve']:>6}"
139+
)
140+
141+
142+
if __name__ == "__main__":
143+
main()

0 commit comments

Comments
 (0)