Первым запросом мы выбираем из базы все узлы со значением
false
для свойства blacklisted
, а вторым уже запрашиваем короткий путь до группы доменных администраторов, исключая из него полученные в первом запросе узлы (рис. 3.27).Добавлять свойство
blacklisted
можно и связям. Установим это свойство для связи между comp.domain.local
и admin
(рис. 3.28):MATCH p=(c: Computer {name: "COMP.DOMAIN.LOCAL"})-[r: HasSession]-(u: User {name: "ADMIN@DOMAIN.LOCAL"}) SET r.blacklisted = TRUE RETURN p
А теперь выполним запрос, где исключим из результата связи, помеченные свойством
blacklisted
.MATCH p=shortestPath((n)-[*1..]->(m: Group {name: "DOMAIN ADMINS@DOMAIN.LOCAL"}))
WHERE NOT n=m AND NONE (x IN relationships(p) WHERE x.blacklisted IS NOT NULL)
RETURN p
Оператор FOREACHЕдинственный случай, когда вам нужно выполнить итерацию, – это работа с коллекциями. В предыдущей главе мы видели, что Cypher может использовать разные виды коллекций – коллекции узлов, отношений и свойств. Иногда вам может потребоваться перебрать коллекцию и последовательно выполнить некоторую операцию записи. Для этой цели существует операция
FOREACH
.MATCH (u: User) WITH collect(u) AS User
FOREACH (n IN User | SET n.test = TRUE)
Функции HEAD, TAIL и LASTФункция
HEAD
возвращает первое значение в списке, TAIL
– все остальные (кроме первого), а LAST
– последнее значение (рис. 3.29).MATCH (c: Computer) WITH collect(c.name) AS comps
RETURN HEAD(comps), TAIL(comps), LAST(comps)
Рис. 3.27. Результат исправленного запроса
Рис. 3.28. Результат запроса с blacklisted связью
Рис. 3.29. Результат использования HEAD, TAIL и LAST
Условие «если… то»
Во время анализа и обновления данных может потребоваться условие «если… то». В Cypher нет привычных
if
и else
, тут используется другая конструкция:CASE WHEN
THEN
ELSE
END
В качестве примера рассмотрим ситуацию, в которой у нас есть учетные записи пользователей, от которых известен пароль, и нам нужно установить для них свойство компрометации узла. В разделе изменения свойств мы уже добавляли и удаляли свойство
password
. Поэтому для рассмотрения условия «если… то» добавим пароли для двух учетных записей:MATCH (u: User) WHERE u.name =~ "(ADMIN@).*" SET u.password = "Password1";
MATCH (u: User) WHERE u.name =~ "(USER@).*" SET u.password = "Password2"
Теперь установим свойству
owned
значение TRUE
для всех пользователей, у которых есть ненулевое свойство password
, а для остальных пользователей – значение FALSE
. Сделаем это с помощью следующего запроса Cypher (рис. 3.30):MATCH(u: User) WITH *,
CASE WHEN u.password IS NOT NULL
THEN TRUE
ELSE FALSE
END as result
SET u.owned = result
RETURN u.name, u.password, u.owned
В сочетании с оператором
IN
можно изменить значение элемента в списке:MATCH (c: Computer) WHERE c.objectid ENDS WITH "-1103"
SET c.ports = [x IN c.ports | CASE WHEN x = "3389" THEN "5985" ELSE x END]
RETURN c.name, c.ports
Рис. 3.30. Результат выполнения запроса
Работа со временем
В neo4j есть встроенные функции для работы со временем. Например:
RETURN date(), datetime(), time()
Существует ряд других типов данных, таких как
Time
, LocalTime
, LocalDateTime
и Timestamp
. Из функций также можно извлекать отдельные свойства, например год:RETURN datetime(). year
Чтобы получить разницу между двумя объектами, используется функция
duration
:WITH date('2024–03–16') AS date1, date('2024–04–16') AS date2
RETURN duration.between(date1, date2)
Или то же самое, но в днях:
WITH date('2024–03–16') AS date1, date('2024–04–16') AS date2
RETURN duration.inDays(date2, date1). days
Также можно прибавлять или убавлять дни, месяцы, года и т. д.:
WITH date('2024–03–16') AS date1, date('2024–04–16') AS date2
RETURN date1 – duration({days:5}), date2 + duration({months:6})
В пользовательском интерфейсе BloodHound даты отображаются в удобочитаемом формате. Однако в базе данных они хранятся в формате эпохи (
unix time
), поэтому в запросах со свойствами pwdlastset
, lastlogon
, lastlogontimestamp
, whencreated
необходимо использовать формат эпохи.RETURN datetime(). epochseconds AS epoch
Перевести формат
epoch
обратно в datetime
можно с помощью следующего запроса:RETURN datetime({epochseconds:1710747114}) AS DateTime
Разница между датами в эпохах может быть получена следующим образом:
RETURN (datetime()-duration({days:90})). epochseconds
Или мы можем просто вычесть количество секунд в 90 днях:
RETURN datetime(). epochseconds – (90*24*60*60) AS Ago90
Теперь рассмотрим более практичные задачи: получим дату установки пароля для пользователей и переведем его в читаемый вид.
MATCH(u: User) u.pwdlastset IS NOT NULL
RETURN u.name, datetime({epochseconds: toInteger(u.pwdlastset)}) as pwdlastset
В этом запросе мы переводим свойство
pwdlastset
в тип Long
с помощью toInteger
, так как сейчас это свойство определяется как Double
.Существует вариант с использованием встроенных процедур:
MATCH(u: User) RETURN u.name, apoc.date.toISO8601(u.pwdlastset,'s') as pwdlastset
Здесь используется процедура
apoc.date.toISO8601
для перевода эпохи в читаемое время в формате ISO8601.Теперь вернемся к разнице и посмотрим, для каких учетных записей пароль устанавливался больше чем 10 дней назад:
MATCH (u: User) WHERE u.pwdlastset < (datetime()-duration({days:10})). epochseconds RETURN u.name
К предыдущему запросу добавим информацию, когда пароль сменился, в днях:
MATCH (u: User) WHERE u.pwdlastset < (datetime()-duration({days:10})). epochseconds
RETURN u.name, duration.inDays(datetime({epochseconds: toInteger(u.pwdlastset)}), datetime()). days
Тот же самый запрос, но с использованием процедуры
apoc.date.toISO8601
:MATCH (u: User) WHERE u.pwdlastset < (datetime()-duration({days:10})). epochseconds
RETURN u.name AS Name, duration.inDays(datetime(apoc.date.toISO8601(u.pwdlastset, 's')), datetime()). days as Days
Можно добавить свойство даты и времени компрометации узла вместе с установкой свойства
owned
в значение TRUE
.Если точное время не требуется и будет достаточно даты, то запрос будет следующим:
MATCH (u: User {name: "USER@DOMAIN.LOCAL"}) SET u.owned = TRUE, u.owneddate = datetime(). epochseconds RETURN u
Или можно установить точную дату и время с использованием формата даты ISO8601:
WITH '2024–04–15T18:33:05' AS owneddate
MATCH (u: User {name: "USER@DOMAIN.LOCAL"})
SET u.owned = TRUE, u.owneddate = datetime(owneddate). epochseconds RETURN u
Информация
Хотя перевод в эпохи необязателен, здесь мы просто придерживаемся общего принципа работы с датами для BloodHound.