Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Neo4j >= 4 compatibility (shortestonly and dijkstra-cypher algorithms only) #9

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aclpwn/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def main():
paths = pathfinding.dijkstra_find_cypher(from_object, to_object, args.from_type.capitalize(), args.to_type.capitalize())
else:
# First we need to obtain the node IDs for use with the REST api
q = "MATCH (n:%s {name: {name}}) RETURN n"
q = "MATCH (n:%s {name: $name}) RETURN n"
with database.driver.session() as session:
fromres = session.run(q % args.from_type.capitalize(), name=from_object)
try:
Expand Down
2 changes: 1 addition & 1 deletion aclpwn/database.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import unicode_literals, print_function
from neo4j.v1 import GraphDatabase
from neo4j import GraphDatabase
import platform
import requests
import json
Expand Down
49 changes: 25 additions & 24 deletions aclpwn/pathfinding.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,36 +39,36 @@ def dijkstra_find(fromid, toid, dbhost):
return paths

def dijkstra_find_cypher(startnode, endnode, starttype='User', endtype='User'):
query = "MATCH (n:%s {name: {startnode}}), (m:%s {name: {endnode}}) " \
"CALL algo.shortestPath.stream(n, m, 'aclpwncost', " \
"{nodeQuery:null, relationshipQuery:null, defaultValue:200.0, direction:'OUTGOING'}) " \
"YIELD nodeId, cost " \
"RETURN nodeId as node, cost"
query = "MATCH (n:%s {name: $startnode}), (m:%s {name: $endnode}) " \
"CALL gds.beta.shortestPath.dijkstra.stream({sourceNode: id(n), targetNode: id(m), " \
"relationshipWeightProperty: 'aclpwncost', nodeProjection: '*', relationshipProjection: {" \
"all: {type: '*', properties: 'aclpwncost', orientation: 'NATURAL'}}}) " \
"YIELD nodeIds, costs " \
"RETURN nodeIds, costs"

path = []
with database.driver.session() as session:
with session.begin_transaction() as tx:
print(query % (starttype, endtype))
path = tx.run(query % (starttype, endtype),
startnode=startnode,
endnode=endnode,
property='aclpwncost')
for record in tx.run(query % (starttype, endtype), startnode=startnode, endnode=endnode, property='aclpwncost'):
path.append(record)
paths = []
nodes, rels = resolve_dijkstra_path(path)
paths.append((nodes, rels, path))
if path:
nodes, rels = resolve_dijkstra_path(path[0])
paths.append((nodes, rels, path[0]))
return paths


queries = {
# Query all shortest paths
'shortestonly': "MATCH (n:%s {name: {startnode}}),"
"(m:%s {name: {endnode}}),"
'shortestonly': "MATCH (n:%s {name: $startnode}),"
"(m:%s {name: $endnode}),"
" p=allShortestPaths((n)-[:MemberOf|AddMember|GenericAll|"
"GenericWrite|WriteOwner|WriteDacl|Owns|DCSync|GetChangesAll|AllExtendedRights*1..]->(m))"
" RETURN p",
# Query all simple paths (more expensive query than above)
# credits to https://stackoverflow.com/a/40062243
'allsimple': "MATCH (n:%s {name: {startnode}}),"
"(m:%s {name: {endnode}}),"
'allsimple': "MATCH (n:%s {name: $startnode}),"
"(m:%s {name: $endnode}),"
" p=(n)-[:MemberOf|AddMember|GenericAll|"
"GenericWrite|WriteOwner|WriteDacl|Owns|DCSync|GetChangesAll|AllExtendedRights*1..]->(m)"
"WHERE ALL(x IN NODES(p) WHERE SINGLE(y IN NODES(p) WHERE y = x))"
Expand All @@ -77,11 +77,12 @@ def dijkstra_find_cypher(startnode, endnode, starttype='User', endtype='User'):


def get_path(startnode, endnode, starttype='User', endtype='User', querytype='allsimple'):
records = []
with database.driver.session() as session:
with session.begin_transaction() as tx:
return tx.run(queries[querytype] % (starttype, endtype),
startnode=startnode,
endnode=endnode)
for record in tx.run(queries[querytype] % (starttype, endtype), startnode=startnode, endnode=endnode):
records.append(record)
return records

def get_path_cost(record):
nmap = utils.getnodemap(record['p'].nodes)
Expand All @@ -93,17 +94,17 @@ def get_path_cost(record):
def resolve_dijkstra_path(path):
nodes = []
rels = []
nq = "MATCH (n)-[w {aclpwncost: {cost}}]->(m) WHERE ID(n) = {source} AND ID(m) = {dest} RETURN n,w,m"
bnq = "MATCH (n)-[w]->(m) WHERE ID(n) = {source} AND ID(m) = {dest} RETURN n,w,m"
nq = "MATCH (n)-[w {aclpwncost: $cost}]->(m) WHERE ID(n) = $source AND ID(m) = $dest RETURN n,w,m"
bnq = "MATCH (n)-[w]->(m) WHERE ID(n) = $source AND ID(m) = $dest RETURN n,w,m"
with database.driver.session() as session:
with session.begin_transaction() as tx:
pv = path.values()
for i in range(1, len(pv)):
res = tx.run(nq, source=pv[i-1][0], cost=pv[i][1]-pv[i-1][1], dest=pv[i][0])
for i in range(1, len(pv[0])):
res = tx.run(nq, source=pv[0][i-1], cost=pv[1][i]-pv[1][i-1], dest=pv[0][i])
data = res.single()
# No result, most likely an invalid path, but query for a relationship with any cost regardless
if not data:
res = tx.run(bnq, source=pv[i-1][0], dest=pv[i][0])
res = tx.run(bnq, source=pv[0][i-1], dest=pv[0][i])
data = res.single()
nodes.append(data['n'])
rels.append(data['w'])
Expand Down
4 changes: 2 additions & 2 deletions aclpwn/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def print_path(record):
# Iterate the path
pathtext = '(%s)-' % record['p'].nodes[0].get('name')
for el in record['p']:
pathtext += '[%s]->(%s)-' % (el.type, nmap[el.end].get('name'))
pathtext += '[%s]->(%s)-' % (el.type, nmap[el.end_node.id].get('name'))
pathtext = pathtext[:-1]
return pathtext

Expand All @@ -20,7 +20,7 @@ def build_path(record):
nmap = getnodemap(record['p'].nodes)
# Iterate the path
for el in record['p']:
path.append((el, nmap[el.end]))
path.append((el, nmap[el.end_node.id]))
return path

def build_rest_path(nodes, rels):
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
neo4j-driver
impacket
ldap3
requests