├── lib
├── __init__.py
├── aws
│ ├── __init__.py
│ ├── profile.py
│ ├── resources.py
│ └── policy.py
├── util
│ ├── __init__.py
│ ├── keywords.py
│ └── console.py
└── graph
│ ├── edges.py
│ ├── nodes.py
│ ├── base.py
│ └── db.py
├── www
├── .browserslistrc
├── babel.config.js
├── public
│ ├── favicon.ico
│ └── index.html
├── postcss.config.js
├── src
│ ├── assets
│ │ ├── logo.png
│ │ └── fonts
│ │ │ ├── MaterialIcons-Regular.woff
│ │ │ └── MaterialIcons-Regular.woff2
│ ├── main.js
│ ├── App.vue
│ ├── components
│ │ ├── TemplateSelectSearch.vue
│ │ ├── TemplateSelectItem.vue
│ │ ├── SearchResultsTable.vue
│ │ ├── Search.vue
│ │ ├── TemplateAutocomplete.vue
│ │ ├── Menu.vue
│ │ ├── SearchAdvancedFilter.vue
│ │ ├── Database.vue
│ │ └── Properties.vue
│ ├── queries.js
│ ├── codemirror-cypher
│ │ └── cypher-codemirror.css
│ ├── config.js
│ └── neo4j.js
├── .eslintrc.json
└── package.json
├── data
└── sample.zip
├── .gitignore
├── Dockerfile
├── README.md
├── INSTALL
└── cli.py
/lib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/aws/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/util/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/www/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/data/sample.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReversecLabs/awspx/HEAD/data/sample.zip
--------------------------------------------------------------------------------
/www/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/www/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReversecLabs/awspx/HEAD/www/public/favicon.ico
--------------------------------------------------------------------------------
/www/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/www/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReversecLabs/awspx/HEAD/www/src/assets/logo.png
--------------------------------------------------------------------------------
/www/src/assets/fonts/MaterialIcons-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReversecLabs/awspx/HEAD/www/src/assets/fonts/MaterialIcons-Regular.woff
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | data/*
6 | package-lock.json
7 | node_modules
8 | dist
9 | nohup.out
10 |
--------------------------------------------------------------------------------
/www/src/assets/fonts/MaterialIcons-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReversecLabs/awspx/HEAD/www/src/assets/fonts/MaterialIcons-Regular.woff2
--------------------------------------------------------------------------------
/www/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuetify from 'vuetify'
3 | import App from './App.vue'
4 | import neo4j from './neo4j';
5 |
6 | import 'vuetify/dist/vuetify.min.css'
7 | import '@mdi/font/css/materialdesignicons.css'
8 |
9 | Vue.use(neo4j);
10 | Vue.use(Vuetify);
11 | const opts = {}
12 |
13 | Vue.config.productionTip = false
14 |
15 | const vuetify = new Vuetify(opts);
16 |
17 | new Vue({
18 | vuetify,
19 | render: h => h(App)
20 | }).$mount('#app')
--------------------------------------------------------------------------------
/www/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:vue/essential"
9 | ],
10 | "globals": {
11 | "Atomics": "readonly",
12 | "SharedArrayBuffer": "readonly"
13 | },
14 | "parserOptions": {
15 | "ecmaVersion": 2018,
16 | "sourceType": "module"
17 | },
18 | "plugins": [
19 | "vue"
20 | ],
21 | "rules": {
22 | }
23 | }
--------------------------------------------------------------------------------
/www/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
19 |
20 |
30 |
--------------------------------------------------------------------------------
/www/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | awspx
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/www/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "awspx",
3 | "version": "1.3.0",
4 | "description": "AWS resource, relationship, and attack path visualisation",
5 | "author": "beatro0t",
6 | "private": true,
7 | "scripts": {
8 | "serve": "vue-cli-service serve --port 80",
9 | "build": "vue-cli-service build"
10 | },
11 | "dependencies": {
12 | "codemirror": "^5.62.2",
13 | "core-js": "^3.16.1",
14 | "cytoscape": "^3.19.0",
15 | "cytoscape-dagre": "^2.3.2",
16 | "neo4j-driver": "^4.3.2",
17 | "vue": "^2.6.14",
18 | "vue-template-compiler": "^2.6.14",
19 | "vuetify": "^2.5.8"
20 | },
21 | "devDependencies": {
22 | "@mdi/font": "^5.9.55",
23 | "@vue/cli-plugin-babel": "^4.5.13",
24 | "@vue/cli-service": "^4.5.13"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/graph/edges.py:
--------------------------------------------------------------------------------
1 | from lib.graph.base import Edge, json
2 |
3 |
4 | class Associative(Edge):
5 |
6 | def __init__(self, properties={}, source=None, target=None):
7 | super().__init__(properties, source, target)
8 |
9 |
10 | class Transitive(Edge):
11 |
12 | def __init__(self, properties={}, source=None, target=None):
13 | super().__init__(properties, source, target)
14 |
15 |
16 | class Action(Edge):
17 |
18 | def __init__(self, properties={}, source=None, target=None):
19 |
20 | for key in ["Name", "Description", "Effect", "Access", "Reference", "Condition"]:
21 | if key not in properties:
22 | raise ValueError("Edge properties must include '%s'" % key)
23 |
24 | super().__init__(properties, source, target)
25 |
26 |
27 | class Trusts(Action):
28 |
29 | def __init__(self, properties={}, source=None, target=None):
30 |
31 | super().__init__(properties, source, target)
32 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM neo4j:4.3.2-community
2 |
3 | COPY . /opt/awspx
4 | WORKDIR /opt/awspx
5 |
6 | ENV NEO4J_AUTH=neo4j/password
7 | ENV EXTENSION_SCRIPT=/opt/awspx/INSTALL
8 |
9 | RUN apt -y update && apt install -y \
10 | awscli \
11 | nodejs \
12 | npm \
13 | python3-pip \
14 | procps \
15 | git \
16 | && rm -rf /var/lib/apt/lists/* \
17 | && pip3 install --upgrade \
18 | argparse \
19 | awscli \
20 | boto3 \
21 | configparser \
22 | git-python \
23 | neo4j \
24 | rich \
25 | && npm install -g npm@latest
26 |
27 | RUN cd /opt/awspx/www && npm install
28 | RUN gosu neo4j wget -q --timeout 300 --tries 30 --output-document=/var/lib/neo4j/plugins/apoc.jar \
29 | https://github.com/neo4j-contrib/neo4j-apoc-procedures/releases/download/4.3.0.0/apoc-4.3.0.0-all.jar \
30 | && chmod 644 /var/lib/neo4j/plugins/apoc.jar
31 |
32 | VOLUME /opt/awspx/data
33 | EXPOSE 7373 7474 7687 80
34 |
--------------------------------------------------------------------------------
/lib/util/keywords.py:
--------------------------------------------------------------------------------
1 |
2 | from lib.aws.attacks import definitions
3 | from lib.aws.ingestor import *
4 | from lib.aws.profile import Profile
5 | from lib.aws.actions import ACTIONS
6 | from lib.aws.resources import Resources
7 | from lib.graph.edges import *
8 | from lib.graph.nodes import *
9 |
10 |
11 | class Keywords:
12 |
13 | service = [_.__name__ for _ in Ingestor.__subclasses__()]
14 | resource = [k.split(':')[-1] for k in Resources.types]
15 | node = [_.__name__ for _ in Node.__subclasses__()]
16 | edge = [_.__name__ for _ in Edge.__subclasses__()]
17 | region = list(Profile.regions)
18 | action = list(ACTIONS.keys())
19 | attack = list(definitions.keys())
20 |
21 |
22 | class Regex:
23 |
24 | arn = r'arn:aws:([a-z0-9]+):({Region}|[a-z0-9-]*):({Account}|[0-9]{12}|aws)?:([a-z0-9-]+)([A-Za-z0-9-_\.:/{}]+)?'
25 | resource = r'(AWS(::[A-Za-z0-9-]*){1,2})'
26 | integer = r'[0-9]+'
27 | archive = r'((/opt/awspx/data/)?[0-9]+_[A-Za-z0-9]+.zip)'
28 | database = r'([A-Za-z0-9]+.db)'
29 |
--------------------------------------------------------------------------------
/lib/graph/nodes.py:
--------------------------------------------------------------------------------
1 | from lib.graph.base import Node
2 |
3 |
4 | class Generic(Node):
5 |
6 | def __init__(self, properties={}, labels=[]):
7 |
8 | label = self.__class__.__name__
9 |
10 | super().__init__(properties,
11 | labels + [label] if label not in labels else labels,
12 | "Arn")
13 |
14 |
15 | class Resource(Node):
16 |
17 | def __init__(self, properties={}, labels=[], key="Arn"):
18 |
19 | label = self.__class__.__name__
20 |
21 | super().__init__(properties,
22 | labels + [label] if label not in labels else labels,
23 | key)
24 |
25 | def account(self):
26 | if "Arn" not in self.properties() or len(self.properties()["Arn"].split(':')) < 5:
27 | return None
28 |
29 | return str(self.properties()["Arn"].split(':')[4])
30 |
31 |
32 | class External(Node):
33 |
34 | def __init__(self, properties={}, labels=[], key="Name"):
35 |
36 | label = self.__class__.__name__
37 |
38 | super().__init__(properties,
39 | labels + [label] if label not in labels else labels,
40 | key)
41 |
--------------------------------------------------------------------------------
/www/src/components/TemplateSelectSearch.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 | {{ data.item.name }}
16 |
17 |
18 |
19 |
20 |
45 |
46 |
--------------------------------------------------------------------------------
/www/src/queries.js:
--------------------------------------------------------------------------------
1 | export const queries = [
2 | {
3 | name: "Inventory",
4 | description: "List all resources in your account",
5 | value: [
6 | "MATCH (Source:Resource)",
7 | "RETURN Source.Name AS Name,",
8 | "Source.Arn AS ARN"
9 | ]
10 | },
11 | {
12 | name: "Administrators",
13 | description: "List resources that effectively have full access to your account",
14 | value: [
15 | "MATCH Path=(Source:Resource)-[:TRANSITIVE|ATTACK*1..]->(Target:Admin)",
16 | "RETURN Source.Name AS Name, Source.Arn AS Arn"
17 | ]
18 | },
19 | {
20 | name: "Public access",
21 | description: "Shows actions that can be performed by anyone",
22 | value: [
23 | "MATCH Actions=(Source:`AWS::Account`)-[Action:ACTION]->(Target:Resource)",
24 | "WHERE Source.Name = 'All AWS Accounts'",
25 | "AND Action.Effect = 'Allow'",
26 | "RETURN Actions"
27 | ]
28 | },
29 | {
30 | name: "Public buckets (read access)",
31 | description: "Shows buckets that can be read anonymously",
32 | value: [
33 | "MATCH Actions=(Source:`AWS::Account`)-[Action:ACTION]->(Target:`AWS::S3::Bucket`)",
34 | "WHERE Source.Name = 'All AWS Accounts'",
35 | "AND Action.Access = 'Read'",
36 | "AND Action.Effect = 'Allow'",
37 | "RETURN Actions"
38 | ]
39 | },
40 | {
41 | name: "Public roles (assumable)",
42 | description: "Shows roles that can be assumed anonymously",
43 | value: [
44 | "MATCH Actions=(Source:`AWS::Account`)-[Action:ACTION]->(Target:`AWS::Iam::Role`)",
45 | "WHERE Source.Name = 'All AWS Accounts'",
46 | "AND Action.Effect = 'Allow'",
47 | "AND Action.Name =~ '.*sts:Assume.*'",
48 | "RETURN Actions"
49 | ]
50 | }
51 | ];
52 |
--------------------------------------------------------------------------------
/www/src/components/TemplateSelectItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ data.item.id }}
13 |
14 |
15 |
20 |
21 | mdi-tag
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
69 |
70 |
--------------------------------------------------------------------------------
/lib/aws/profile.py:
--------------------------------------------------------------------------------
1 |
2 | import sys
3 | import os
4 | from awscli.clidriver import CLIDriver, load_plugins
5 | from awscli.customizations.configure.configure import ConfigureCommand
6 | from awscli.customizations.configure import mask_value
7 | from botocore.session import Session
8 | from configparser import ConfigParser
9 |
10 |
11 | class InteractivePrompter(object):
12 |
13 | def __init__(self, console):
14 | self.console = console
15 |
16 | def input(self, prompt, value):
17 | return self.console.input(f"{prompt} [{value}]: ")
18 |
19 | # See awscli/customizations/configure/configure.py
20 |
21 | def get_value(self, current_value, config_name, prompt_text=''):
22 | if config_name in ('aws_access_key_id', 'aws_secret_access_key'):
23 | current_value = mask_value(current_value)
24 | response = self.input(prompt_text, current_value)
25 | if not response:
26 | # If the user hits enter, we return a value of None
27 | # instead of an empty string. That way we can determine
28 | # whether or not a value has changed.
29 | response = None
30 | return response
31 |
32 |
33 | class Profile:
34 |
35 | regions = [
36 | "af-south-1", "ap-east-1", "ap-northeast-1",
37 | "ap-northeast-2", "ap-northeast-3", "ap-south-1",
38 | "ap-southeast-1", "ap-southeast-2", "ca-central-1",
39 | "cn-north-1", "cn-northwest-1", "eu-central-1",
40 | "eu-north-1", "eu-south-1", "eu-west-1",
41 | "eu-west-2", "eu-west-3", "me-south-1",
42 | "sa-east-1", "us-east-1", "us-east-2",
43 | "us-gov-east-1", "us-gov-west-1", "us-west-1",
44 | "us-west-2"
45 | ]
46 |
47 | config_file = os.environ['HOME'] + '/.aws/config'
48 | credentials_file = os.environ['HOME'] + '/.aws/credentials'
49 |
50 | config = ConfigParser()
51 | credentials = ConfigParser()
52 |
53 | def __init__(self, console=None):
54 |
55 | if console is None:
56 | from lib.util.console import console
57 | self.console = console
58 |
59 | self.credentials.read(self.credentials_file)
60 | self.config.read(self.config_file)
61 |
62 | def create(self, profile=None):
63 | self.reconfigure(profile)
64 |
65 | def reconfigure(self, profile=None):
66 |
67 | if profile is None:
68 | return
69 |
70 | # See awscli/clidriver.py
71 | session = Session()
72 | load_plugins(session.full_config.get('plugins', {}),
73 | event_hooks=session.get_component('event_emitter'))
74 |
75 | driver = CLIDriver(session=session)
76 | driver._command_table = driver._build_command_table()
77 | driver._command_table["configure"] = ConfigureCommand(
78 | session,
79 | prompter=InteractivePrompter(self.console)
80 | )
81 |
82 | driver.main(args=["configure", "--profile", profile])
83 |
84 | def list(self):
85 |
86 | self.console.list([{"Profile": p} for p in self.credentials.keys()
87 | if p != "DEFAULT"])
88 |
89 | def delete(self, profile=None):
90 |
91 | if profile is None:
92 | return
93 |
94 | if self.config.has_section(profile):
95 | self.config.remove_section(profile)
96 |
97 | if self.credentials.has_section(profile):
98 | self.credentials.remove_section(profile)
99 |
100 | with open(self.config_file, 'w') as f:
101 | self.config.write(f)
102 |
103 | with open(self.credentials_file, 'w') as f:
104 | self.credentials.write(f)
105 |
106 | self.console.info(f"Profile '{profile}' deleted.")
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | > auspex [ˈau̯s.pɛks] noun: An augur of ancient Rome, especially one who interpreted omens derived from the observation of birds.
4 |
5 | 
6 | 
7 | 
8 |
9 | # Overview
10 |
11 | **awspx** is a graph-based tool for visualizing effective access and resource relationships within AWS. It resolves policy information to determine *what* actions affect *which* resources, while taking into account how these actions may be combined to produce attack paths. Unlike tools like [Bloodhound](https://github.com/BloodHoundAD/BloodHound), awspx requires permissions to function — it is not expected to be useful in cases where these privileges have not been granted.
12 |
13 | ### Table of contents
14 |
15 | - [Getting Started](#getting-started)
16 | - [Installation](#installation)
17 | - [Usage](#usage)
18 | - [Contributing](#contributing)
19 | - [License](#license)
20 |
21 | *For more information, checkout the [awspx Wiki](https://github.com/FSecureLABS/awspx/wiki)*
22 |
23 | # Getting Started
24 |
25 | *For detailed installation instructions, usage, and answers to frequently asked questions, see sections: [Setup](https://github.com/FSecureLABS/awspx/wiki/Setup); [Data Collection](https://github.com/FSecureLABS/awspx/wiki/Data-Collection) and [Exploration](https://github.com/FSecureLABS/awspx/wiki/Data-Exploration); and [FAQs](https://github.com/FSecureLABS/awspx/wiki/FAQs), respectively.*
26 |
27 | ## Installation
28 |
29 | **awspx** can be [installed](https://github.com/FSecureLABS/awspx/wiki/Setup) on either Linux or macOS. *In each case [Docker](https://docs.docker.com/get-docker/) is required.*
30 |
31 | 1. Clone this repo
32 | ```bash
33 | git clone https://github.com/FSecureLABS/awspx.git
34 | ```
35 | 2. Run the `INSTALL` script
36 | ```bash
37 | cd awspx && ./INSTALL
38 | ```
39 |
40 | ## Usage
41 |
42 | **awspx** consists of two main components: the [**ingestor**](https://github.com/FSecureLABS/awspx/wiki/Data-Collection#ingestion), *which collects AWS account data*; and the [**web interface**](https://github.com/FSecureLABS/awspx/wiki/Data-Exploration#overview), *which allows you to explore it*.
43 |
44 | 1. [Run the **ingestor**](https://github.com/FSecureLABS/awspx/wiki/Data-Collection#ingestion) against an account of your choosing. _You will be prompted for [credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html#cli-quick-configuration)._
45 |
46 | ```bash
47 | awspx ingest
48 | ```
49 | _**OR** optionally forgo this step and [load the sample dataset](https://github.com/FSecureLABS/awspx/wiki/Data-Collection#zip-files) instead._
50 |
51 | ```bash
52 | awspx db --load-zip sample.zip
53 | awspx attacks
54 | ```
55 |
56 | 2. Browse to the **web interface** — * by default* — and [explore this environment](https://github.com/FSecureLABS/awspx/wiki/Data-Exploration##usage-examples).
57 |
58 |
59 |
60 |
61 | # Contributing
62 |
63 | This project is in its early days and there's still plenty that can be done. Whether its submitting a fix, identifying bugs, suggesting enhancements, creating or updating documentation, refactoring smell code, or even extending this list — all contributions help and are more than welcome. Please feel free to use your judgement and do whatever you think would benefit the community most.
64 |
65 | *See [Contributing](https://github.com/FSecureLABS/awspx/wiki/Contributing) for more information.*
66 |
67 | # License
68 |
69 | **awspx** is a graph-based tool for visualizing effective access and resource relationships within AWS. (C) 2018-2020 F-SECURE.
70 |
71 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
72 |
73 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
74 |
75 | You should have received a copy of the GNU General Public License along with this program. If not, see .
76 |
--------------------------------------------------------------------------------
/lib/graph/base.py:
--------------------------------------------------------------------------------
1 |
2 | import json
3 | import os
4 | from datetime import datetime
5 |
6 | from lib.aws.actions import ACTIONS
7 | from lib.aws.resources import RESOURCES
8 |
9 | from lib.graph.db import Neo4j
10 |
11 |
12 | class Element:
13 |
14 | def __init__(self, properties={}, labels=[], key="Name"):
15 |
16 | if not isinstance(properties, dict):
17 | raise ValueError()
18 |
19 | if "Name" not in properties:
20 | raise ValueError("All elements must include a name")
21 |
22 | if key not in properties:
23 | raise ValueError("Missing key: '%s'" % key)
24 |
25 | self._properties = {}
26 |
27 | for k, v in properties.items():
28 |
29 | if any([isinstance(v, t) for t in [datetime, dict, list, int]]):
30 | self._properties[k] = v
31 | continue
32 |
33 | elif type(v) is None:
34 | self._properties[k] = ""
35 | continue
36 |
37 | try:
38 | self._properties[k] = json.loads(v)
39 | continue
40 | except json.decoder.JSONDecodeError:
41 | pass
42 |
43 | try:
44 | self._properties[k] = datetime.strptime(
45 | v[:-6], '%Y-%m-%d %H:%M:%S')
46 | continue
47 | except ValueError:
48 | pass
49 |
50 | self._properties[k] = str(v)
51 |
52 | self._labels = set(labels)
53 | self._key = key
54 |
55 | def properties(self):
56 | return self._properties
57 |
58 | def label(self):
59 | return [
60 | *[l for l in self.labels()
61 | if l != self.__class__.__name__
62 | ],
63 | ""
64 | ][0]
65 |
66 | def labels(self):
67 | return sorted(list(self._labels))
68 |
69 | def type(self, label):
70 | return label in self._labels
71 |
72 | def id(self):
73 | return self._properties[self._key]
74 |
75 | def get(self, k):
76 | return self._properties[k]
77 |
78 | def set(self, k, v):
79 | self._properties[k] = v
80 |
81 | def __hash__(self):
82 | return hash(self.id())
83 |
84 | def __eq__(self, other):
85 | if isinstance(other, str):
86 | return other in self.labels()
87 | return self.__hash__() == other.__hash__()
88 |
89 | def __lt__(self, other):
90 | return self.__hash__() < other.__hash__()
91 |
92 | def __gt__(self, other):
93 | return self.__hash__() > other.__hash__()
94 |
95 | def __repr__(self):
96 | return self.id()
97 |
98 | def __str__(self):
99 | return str(self.id())
100 |
101 |
102 | class Node(Element):
103 | def __init__(self, properties={}, labels=[], key="Name"):
104 | super().__init__(properties, labels, key)
105 |
106 |
107 | class Edge(Element):
108 |
109 | def __init__(self, properties={}, source=None, target=None, label=None):
110 |
111 | if label is None:
112 | label = [str(self.__class__.__name__).upper()]
113 |
114 | super().__init__(properties, label)
115 |
116 | self._source = source
117 | self._target = target
118 | self._set_id()
119 |
120 | def _set_id(self):
121 |
122 | self._id = hash("({source})-[:{label}{{{properties}}}]->({target})".format(
123 | source=self.source(),
124 | label=self.labels()[0],
125 | properties=json.dumps(self.properties(), sort_keys=True),
126 | target=self.target())
127 | )
128 |
129 | def source(self):
130 | return self._source
131 |
132 | def target(self):
133 | return self._target
134 |
135 | def id(self):
136 | return self._id
137 |
138 | def modify(self, k, v):
139 | super().set(k, v)
140 | self._set_id()
141 |
142 | def __str__(self):
143 | return str(self.get("Name"))
144 |
145 |
146 | class Elements(set):
147 |
148 | def __init__(self, _=[], load=False, generics=False):
149 |
150 | super().__init__(_)
151 |
152 | def __add__(self, other):
153 | return Elements(self.union(other))
154 |
155 | def __iadd__(self, other):
156 | self.update(other)
157 | return Elements(self)
158 |
159 | def get(self, label):
160 | return Elements(filter(lambda r: r.type(label), self))
161 |
162 | def __repr__(self):
163 | return str([str(e) for e in self])
164 |
--------------------------------------------------------------------------------
/www/src/components/SearchResultsTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
14 | Search results
15 |
16 |
17 |
18 | mdi-close
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | |
28 |
32 | |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
116 |
117 |
163 |
--------------------------------------------------------------------------------
/www/src/components/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 | mdi-chevron-up
16 |
17 |
18 |
19 |
20 |
21 |
32 |
33 |
34 |
47 |
48 |
49 |
50 |
51 |
166 |
167 |
178 |
--------------------------------------------------------------------------------
/www/src/components/TemplateAutocomplete.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
107 |
108 |
109 |
110 |
111 |
112 |
206 |
207 |
--------------------------------------------------------------------------------
/INSTALL:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #####################################################################################################
4 | # #
5 | # This script serves a couple of purposes, its behaviour will vary based on how it is named: #
6 | # #
7 | # - INSTALL - will create the awspx container and copy this script to awspx #
8 | # - awspx - will pass arguments to cli.py in the awpx container #
9 | # - docker-entrypoint.sh - will start the webserver and neo4j (sourced by /docker-entrypoint.sh) #
10 | # #
11 | #####################################################################################################
12 |
13 | _OS_="UNKNOWN"
14 |
15 | function host_checks(){
16 |
17 | #MacOS
18 | if [ "$(uname)" == "Darwin" ]; then
19 | _OS_="MACOS"
20 |
21 | # Linux
22 | elif [[ "$(uname)" =~ "Linux" ]]; then
23 | _OS_="LINUX"
24 | if [[ "$(whoami)" != "root" ]]; then
25 | echo "[-] awspx must be run with root privileges."
26 | exit 1
27 | fi
28 | # Unsupported
29 | else
30 | echo "[-] Platform: '$(uname)' is not supported"
31 | exit 1
32 | fi
33 |
34 | DOCKER_RUNNING="$(docker info >/dev/null 2>&1)"
35 |
36 | if [ "${?}" -ne 0 ]; then
37 | echo "[-] \"docker\" must first be started."
38 | exit 1
39 | fi
40 | }
41 |
42 | function install(){
43 |
44 | BIN_PATH="/usr/local/bin"
45 |
46 | [[ $_OS_ == "MACOS" ]] \
47 | && MOUNT="${HOME}/bin/awspx" \
48 | || MOUNT="/opt/awspx"
49 |
50 | # Use default path for awspx installation
51 | if [[ ":${PATH}:" != *":${BIN_PATH}:"* ]] || [ ! -w "${BIN_PATH}" ]; then
52 | read -p "[*] '${BIN_PATH}' isn't in your \$PATH. Choose another location to save \"awspx\" [y/n]? " response
53 | if [[ "${response}" == "Y" || "${response}" == "y" ]]; then
54 | select p in $(for p in $(echo "${PATH}" | tr ':' '\n' | sort); do [[ -w $p ]] && echo $p; done); do
55 | case $p in
56 | /*)
57 | BIN_PATH="$p"
58 | break
59 | ;;
60 | *) ;;
61 | esac
62 | done
63 | fi
64 | fi
65 |
66 | cp -f $0 ${BIN_PATH}/awspx
67 |
68 | # Assert awspx exists in $PATH
69 | if (which awspx >/dev/null 2>&1) ; then
70 | echo "awspx successfully written to ${BIN_PATH}/awspx"
71 | else
72 | >&2 echo "Failed to identify a writable \$PATH directory"
73 | exit 2
74 | fi
75 |
76 | # Delete all containers named awspx (prompt for confirmation)
77 | if [ -n "$(docker ps -a -f name=awspx -q)" ]; then
78 |
79 | echo -e "[!] An existing container named \"awspx\" was detected\n"
80 | echo -e " In order to continue, it must be deleted. All data will be lost."
81 | read -p " Continue [y/n]? " response
82 |
83 | [[ "${response}" == "Y" || "${response}" == "y" ]] \
84 | || exit
85 |
86 | docker stop awspx >/dev/null 2>&1
87 | docker rm awspx >/dev/null 2>&1
88 |
89 | fi
90 |
91 | echo ""
92 |
93 | # Build or pull awspx
94 | case $1 in
95 | build|BUILD)
96 | echo -e "[*] Creating \"awspx\" image...\n"
97 | docker build $(dirname $0) -t beatro0t/awspx:latest
98 | ;;
99 | *)
100 | echo -e "[*] Pulling \"awspx\" image... \n"
101 | docker pull beatro0t/awspx:latest
102 | ;;
103 | esac
104 |
105 | if [ $? -ne 0 ]; then
106 | echo -e "\n[-] Installation failed"
107 | exit 1
108 | fi
109 |
110 | echo ""
111 |
112 | # Create container
113 | echo -en "[*] Creating \"awspx\" container... "
114 | if docker run -itd \
115 | --name awspx \
116 | --hostname=awspx \
117 | --env NEO4J_AUTH=neo4j/password \
118 | -p 127.0.0.1:80:80 \
119 | -p 127.0.0.1:7687:7687 \
120 | -p 127.0.0.1:7373:7373 \
121 | -p 127.0.0.1:7474:7474 \
122 | -v ${MOUNT}/data:/opt/awspx/data:z \
123 | -e NEO4J_dbms_security_procedures_unrestricted=apoc.jar \
124 | --restart=always beatro0t/awspx:latest >/dev/null; then
125 |
126 | cp $(dirname $0)/data/sample.zip -f ${MOUNT}/data/. >/dev/null 2>&1
127 |
128 | echo -e "and you're all set!\n"
129 |
130 | echo -e " The web interface (http://localhost) will be available shortly..."
131 | echo -e " Run: \`awspx -h\` for a list of options."
132 | fi
133 |
134 | echo ""
135 | }
136 |
137 | function hook(){
138 |
139 | if [[ "${@}" == "neo4j" ]]; then
140 |
141 | # Start web interface
142 | [[ -z "$(pgrep npm)" ]] \
143 | && cd /opt/awspx/www \
144 | && nohup npm run serve>/dev/null 2>&1 &
145 |
146 | # Start neo4j
147 | nohup bash /docker-entrypoint.sh neo4j console 2>&1 &
148 |
149 | # Start bash so /docker-entrypoint.sh doesn't terminate
150 | exec bash
151 | fi
152 |
153 | }
154 |
155 | function awspx(){
156 |
157 | if [[ -z "$(docker ps -a -f name=^/awspx$ -q)" ]]; then
158 | echo -e "[-] Couldn't find \"awspx\" container, you will need to create it first"
159 | exit 1
160 | fi
161 |
162 | if [[ -z "$(docker ps -a -f name=^/awspx$ -f status=running -q)" ]]; then
163 | docker start awspx > /dev/null
164 | fi
165 |
166 | docker exec -it \
167 | -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
168 | -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
169 | -e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN \
170 | -e AWS_SECURITY_TOKEN=$AWS_SECURITY_TOKEN \
171 | awspx /opt/awspx/cli.py $@
172 |
173 | }
174 |
175 | function main(){
176 |
177 | case "$(basename $0)" in
178 | INSTALL)
179 | host_checks
180 | install $@
181 | ;;
182 | docker-entrypoint.sh)
183 | hook $@
184 | ;;
185 | awspx)
186 | host_checks
187 | awspx $@
188 | ;;
189 | esac
190 |
191 | }
192 |
193 | main $@
194 |
--------------------------------------------------------------------------------
/www/src/components/Menu.vue:
--------------------------------------------------------------------------------
1 |
2 |
176 |
177 |
178 |
209 |
210 |
221 |
222 |
--------------------------------------------------------------------------------
/www/src/components/SearchAdvancedFilter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
38 |
39 |
40 |
49 |
50 |
51 |
61 |
62 |
63 |
64 |
65 |
66 |
346 |
347 |
356 |
--------------------------------------------------------------------------------
/lib/graph/db.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import os
3 | import re
4 | import shutil
5 | import subprocess
6 | import sys
7 | import time
8 | import warnings
9 |
10 | from neo4j import ExperimentalWarning, GraphDatabase, exceptions
11 |
12 | warnings.filterwarnings("ignore", category=ExperimentalWarning)
13 |
14 | NEO4J_DB_DIR = "/data/databases"
15 | NEO4J_ZIP_DIR = "/opt/awspx/data"
16 | NEO4J_CONF_DIR = "/var/lib/neo4j/conf"
17 | NEO4J_TRANS_DIR = "/data/transactions"
18 |
19 |
20 | class Neo4j(object):
21 |
22 | driver = None
23 |
24 | zips = [z for z in os.listdir(f"{NEO4J_ZIP_DIR}/")
25 | if z.endswith(".zip")]
26 |
27 | databases = [db for db in os.listdir(f"{NEO4J_DB_DIR}/")
28 | if os.path.isdir(f"{NEO4J_DB_DIR}/{db}")]
29 |
30 | def __init__(self,
31 | host="localhost",
32 | port="7687",
33 | username="neo4j",
34 | password=str(os.environ['NEO4J_AUTH'][6:]
35 | if 'NEO4J_AUTH' in os.environ else "password"),
36 | console=None):
37 |
38 | if console is None:
39 | from lib.util.console import console
40 | self.console = console
41 |
42 | self.uri = f"bolt://{host}:{port}"
43 | self.username = username
44 | self.password = password
45 |
46 | def _start(self):
47 |
48 | retries = 0
49 | max_retries = 60
50 |
51 | while retries < max_retries and not self.available():
52 |
53 | if retries == 0:
54 |
55 | subprocess.Popen(["nohup", "/docker-entrypoint.sh",
56 | "neo4j", "console", "&"],
57 | stdout=subprocess.PIPE,
58 | stderr=subprocess.STDOUT)
59 | time.sleep(1)
60 | retries += 1
61 |
62 | if not self.available():
63 | self.console.critical("Neo4j failed to start")
64 | return False
65 |
66 | elif retries == 0:
67 | self.console.info("Neo4j has already been started")
68 | else:
69 | self.console.info("Neo4j has successfully been started")
70 |
71 | return True
72 |
73 | def _stop(self):
74 |
75 | retries = 0
76 | max_retries = 10
77 |
78 | while retries < max_retries and self.running():
79 | subprocess.Popen(["killall", "java"])
80 | time.sleep(1)
81 | retries += 1
82 |
83 | if self.running():
84 | self.console.critical("Neo4j failed to stop")
85 | return False
86 |
87 | subprocess.Popen(["rm", "-f", f"{NEO4J_DB_DIR}/store_lock",
88 | f"{NEO4J_DB_DIR}/system/database_lock"],
89 | stdout=subprocess.PIPE,
90 | stderr=subprocess.STDOUT)
91 |
92 | if retries == 0:
93 | self.console.info("Neo4j has already been stopped")
94 | else:
95 | self.console.info("Neo4j has successfully been stopped")
96 |
97 | return True
98 |
99 | def _delete(self, db):
100 |
101 | subprocess.Popen(["rm", "-rf", f"{NEO4J_DB_DIR}/{db}",
102 | f"{NEO4J_TRANS_DIR}/{db}"])
103 |
104 | def _run(self, tx, cypher):
105 | results = tx.run(cypher)
106 | return results
107 |
108 | def _switch_database(self, db):
109 |
110 | subprocess.Popen(["sed", "-i",
111 | '/^\(#\)\{0,1\}dbms.default_database=/s/.*/dbms.default_database=%s/' % db,
112 | f"{NEO4J_CONF_DIR}/neo4j.conf"
113 | ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
114 | ).communicate()
115 |
116 | def _load(self, archives, db):
117 |
118 | ARCHIVES = {}
119 |
120 | for archive in archives:
121 |
122 | ARCHIVES[archive] = {
123 | "DIR": None,
124 | "CSV": None,
125 | }
126 |
127 | ARCHIVES[archive]["DIR"] = archive.split('.')[0]
128 | shutil.unpack_archive(archive, ARCHIVES[archive]["DIR"], "zip")
129 |
130 | ARCHIVES[archive]["CSV"] = [f for f in os.listdir(ARCHIVES[archive]["DIR"])
131 | if f.endswith(".csv")]
132 |
133 | for c in set([c for a in ARCHIVES
134 | for c in ARCHIVES[a]["CSV"]
135 | if len(ARCHIVES) > 0]):
136 |
137 | keys = set()
138 |
139 | for i in range(2):
140 |
141 | for _, v in ARCHIVES.items():
142 |
143 | if c not in v["CSV"]:
144 | continue
145 |
146 | else:
147 |
148 | with open(f'{v["DIR"]}/{c}', 'r') as f:
149 | headers = [h.strip()
150 | for h in f.readline().split(',')]
151 |
152 | if i == 0:
153 | keys.update(headers)
154 | continue
155 |
156 | additional = [k for k in keys if k not in headers]
157 |
158 | if not len(additional) > 0:
159 | continue
160 |
161 | self.console.debug(f"Adding columns {additional} "
162 | f'to {v["DIR"]}/{c}')
163 |
164 | with open(f'{v["DIR"]}/{c}', 'r') as f:
165 | rows = f.read().splitlines()
166 |
167 | rows[0] = ','.join(rows[0].split(',') + additional)
168 |
169 | for i in range(1, len(rows)):
170 | rows[i] = ','.join(rows[i].split(
171 | ',') + ['' for _ in additional])
172 |
173 | with open(f'{v["DIR"]}/{c}', 'w') as f:
174 | f.write('\n'.join(rows))
175 |
176 | csvs = [f"{a['DIR']}/{csv}" for a in ARCHIVES.values()
177 | for csv in a["CSV"]]
178 |
179 | edges = [e for e in csvs
180 | if re.compile("(.*/)?([A-Z]+)\.csv").match(e)]
181 |
182 | nodes = [n for n in csvs
183 | if n not in edges]
184 |
185 | self._delete(db)
186 |
187 | stdout, _ = subprocess.Popen(["/docker-entrypoint.sh", "neo4j-admin", "import",
188 | "--report-file", "/dev/null",
189 | "--skip-duplicate-nodes", "true",
190 | "--skip-bad-relationships", "true",
191 | "--multiline-fields=true",
192 | f"--database={db}",
193 | *[f"--nodes={n}" for n in nodes],
194 | *[f"--relationships={e}" for e in edges]],
195 | stdout=subprocess.PIPE,
196 | stderr=subprocess.STDOUT).communicate()
197 |
198 | subprocess.Popen(["rm", "-rf", *[a["DIR"] for a in ARCHIVES.values()]])
199 |
200 | stats = re.compile("([0-9a-zA-Z]+)."
201 | "Imported:([0-9]+)nodes"
202 | "([0-9]+)relationships"
203 | "([0-9]+)properties"
204 | "[A-Za-z ]+:(.*)"
205 | ).match(str(stdout).split("IMPORT DONE in ")[-1]
206 | .replace("\\n", "").replace(" ", ""))
207 |
208 | if stats is None:
209 | self.console.critical(str(stdout).replace(
210 | "\\n", "\n").replace("\\t", "\t"))
211 |
212 | (time, nodes, edges, props, ram) = stats.groups()
213 |
214 | return str(f"Loaded {nodes} nodes, {edges} edges, and {props} properties "
215 | f"into '{db}' from: {', '.join([re.sub(f'^{NEO4J_ZIP_DIR}/', '', a) for a in archives])}")
216 |
217 | def running(self):
218 |
219 | stdout, _ = subprocess.Popen(['pgrep', 'java'],
220 | stdout=subprocess.PIPE,
221 | stderr=subprocess.STDOUT).communicate()
222 |
223 | pids = [int(i) for i in stdout.split()]
224 |
225 | return len(pids) > 0
226 |
227 | def open(self):
228 |
229 | self.driver = GraphDatabase.driver(
230 | self.uri,
231 | auth=(self.username, self.password)
232 | )
233 |
234 | def close(self):
235 | if self.driver is not None:
236 | self.driver.close()
237 | self.driver = None
238 |
239 | def available(self):
240 | try:
241 | self.open()
242 | self.driver.verify_connectivity()
243 | except Exception:
244 | return False
245 | return True
246 |
247 | def use(self, db):
248 |
249 | self.console.task("Stopping Neo4j",
250 | self._stop, done="Stopped Neo4j")
251 |
252 | self.console.task(f"Switching database to {db}",
253 | self._switch_database, args=[db],
254 | done=f"Switched database to '{db}'")
255 |
256 | self.console.task("Starting Neo4j",
257 | self._start, done="Started Neo4j")
258 |
259 | def load_zips(self, archives=[], db='neo4j'):
260 |
261 | archives = [f"{NEO4J_ZIP_DIR}/{a}"
262 | if not a.startswith(f"{NEO4J_ZIP_DIR}/") else a
263 | for a in archives]
264 |
265 | self.console.task("Stopping Neo4j",
266 | self._stop, done="Stopped Neo4j")
267 |
268 | loaded = self.console.task(f"Creating database '{db}'",
269 | self._load, args=[archives, db],
270 | done=f"Created database '{db}'")
271 |
272 | self.console.task(f"Switching active database to '{db}'",
273 | self._switch_database, args=[db],
274 | done=f"Switched active database to '{db}'")
275 |
276 | self.console.task("Starting Neo4j",
277 | self._start, done="Started Neo4j")
278 |
279 | self.console.notice(loaded)
280 |
281 | def list(self):
282 |
283 | self.console.list([{
284 | "Name": db,
285 | "Created": datetime.datetime.strptime(
286 | time.ctime(os.path.getctime(f"{NEO4J_DB_DIR}/{db}")),
287 | "%a %b %d %H:%M:%S %Y"
288 | ).strftime('%Y-%m-%d %H:%M:%S')
289 | } for db in self.databases])
290 |
291 | def run(self, cypher):
292 |
293 | results = []
294 |
295 | if not self.available():
296 | self._start()
297 |
298 | try:
299 | with self.driver.session() as session:
300 | results = session.run(cypher).data()
301 |
302 | except exceptions.CypherSyntaxError as e:
303 | self.console.error(str(e))
304 |
305 | return results
306 |
--------------------------------------------------------------------------------
/www/src/codemirror-cypher/cypher-codemirror.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2002-2017 "Neo Technology,"
3 | * Network Engine for Objects in Lund AB [http://neotechnology.com]
4 | *
5 | * This file is part of Neo4j.
6 | *
7 | * Neo4j is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU General Public License as published by
9 | * the Free Software Foundation, either version 3 of the License, or
10 | * (at your option) any later version.
11 | *
12 | * This program is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU General Public License
18 | * along with this program. If not, see .
19 | */
20 |
21 |
22 | /*
23 | Credits: http://ethanschoonover.com/solarized
24 |
25 | SOLARIZED HEX 16/8 TERMCOL XTERM/HEX L*A*B RGB HSB
26 | --------- ------- ---- ------- ----------- ---------- ----------- -----------
27 | base03 #002b36 8/4 brblack 234 #1c1c1c 15 -12 -12 0 43 54 193 100 21
28 | base02 #073642 0/4 black 235 #262626 20 -12 -12 7 54 66 192 90 26
29 | base01 #586e75 10/7 brgreen 240 #585858 45 -07 -07 88 110 117 194 25 46
30 | base00 #657b83 11/7 bryellow 241 #626262 50 -07 -07 101 123 131 195 23 51
31 | base0 #839496 12/6 brblue 244 #808080 60 -06 -03 131 148 150 186 13 59
32 | base1 #93a1a1 14/4 brcyan 245 #8a8a8a 65 -05 -02 147 161 161 180 9 63
33 | base2 #eee8d5 7/7 white 254 #e4e4e4 92 -00 10 238 232 213 44 11 93
34 | base3 #fdf6e3 15/7 brwhite 230 #ffffd7 97 00 10 253 246 227 44 10 99
35 | yellow #b58900 3/3 yellow 136 #af8700 60 10 65 181 137 0 45 100 71
36 | orange #cb4b16 9/3 brred 166 #d75f00 50 50 55 203 75 22 18 89 80
37 | red #dc322f 1/1 red 160 #d70000 50 65 45 220 50 47 1 79 86
38 | magenta #d33682 5/5 magenta 125 #af005f 50 65 -05 211 54 130 331 74 83
39 | violet #6c71c4 13/5 brmagenta 61 #5f5faf 50 15 -45 108 113 196 237 45 77
40 | blue #268bd2 4/4 blue 33 #0087ff 55 -10 -45 38 139 210 205 82 82
41 | cyan #2aa198 6/6 cyan 37 #00afaf 60 -35 -05 42 161 152 175 74 63
42 | green #859900 2/2 green 64 #5f8700 60 -20 65 133 153 0 68 100 60
43 | */
44 |
45 |
46 | /***********
47 | * Editor
48 | */
49 |
50 | .CodeMirror.cm-s-cypher {
51 | background-color: white;
52 | line-height: 1.4375;
53 | color: #657b83;
54 | }
55 |
56 | .CodeMirror.cm-s-cypher.cm-s-cypher-dark {
57 | background-color: #002b36;
58 | color: #839496;
59 | }
60 |
61 | .cm-s-cypher pre {
62 | padding: 0;
63 | }
64 |
65 | .cm-s-cypher .CodeMirror-lines {
66 | padding: 0;
67 | }
68 |
69 | .cm-s-cypher .CodeMirror-cursor {
70 | width: auto;
71 | border: 0;
72 | background: rgba(58, 63, 63, 0.63);
73 | z-index: 1;
74 | }
75 |
76 | .cm-s-cypher.cm-s-cypher-dark .CodeMirror-cursor {
77 | background: rgba(51, 58, 59, 0.63);
78 | }
79 |
80 |
81 | /***********
82 | * Gutter
83 | */
84 |
85 | .cm-s-cypher .CodeMirror-gutters {
86 | border-right: 1px solid rgb(25, 118, 210, 0.1);
87 | padding-left: 1px;
88 | padding-right: 3px;
89 | background-color: rgb(25, 118, 210, 0.04);
90 | }
91 |
92 | .cm-s-cypher.cm-s-cypher-dark .CodeMirror-gutters {
93 | background-color: #515151;
94 | border-right: 3px solid #515151;
95 | }
96 |
97 | .cm-s-cypher .CodeMirror-linenumber {
98 | padding-left: 2px;
99 | padding-right: 5px;
100 | color: rgb(25, 118, 210);
101 | }
102 |
103 | .cm-s-cypher.cm-s-cypher-dark .CodeMirror-linenumber {
104 | color: #839496;
105 | }
106 |
107 |
108 | /***********
109 | * Token
110 | */
111 |
112 | .cm-s-cypher .cm-comment {
113 | color: #93a1a1;
114 | }
115 |
116 | .cm-s-cypher.cm-s-cypher-dark .cm-comment {
117 | color: #586e75;
118 | }
119 |
120 | .cm-s-cypher .cm-string {
121 | color: #b58900;
122 | }
123 |
124 | .cm-s-cypher .cm-number {
125 | color: #2aa198;
126 | }
127 |
128 |
129 | /*
130 | .cm-s-cypher .cm-operator {}
131 | */
132 |
133 | .cm-s-cypher .cm-keyword {
134 | color: #859900;
135 | }
136 |
137 |
138 | /***********
139 | * Parser
140 | */
141 |
142 | .cm-s-cypher .cm-p-label {
143 | color: #cb4b16;
144 | }
145 |
146 | .cm-s-cypher .cm-p-relationshipType {
147 | color: #cb4b16;
148 | }
149 |
150 | .cm-s-cypher .cm-p-variable {
151 | color: #268bd2;
152 | }
153 |
154 | .cm-s-cypher .cm-p-procedure {
155 | color: #6c71c4;
156 | }
157 |
158 | .cm-s-cypher .cm-p-function {
159 | color: #6c71c4;
160 | }
161 |
162 | .cm-s-cypher .cm-p-parameter {
163 | color: #dc322f;
164 | }
165 |
166 | .cm-s-cypher .cm-p-property {
167 | color: #586e75;
168 | }
169 |
170 | .cm-s-cypher.cm-s-cypher-dark .cm-p-property {
171 | color: #93a1a1;
172 | }
173 |
174 | .cm-s-cypher .cm-p-consoleCommand {
175 | color: #d33682;
176 | }
177 |
178 | .cm-s-cypher .cm-p-procedureOutput {
179 | color: #268bd2;
180 | }
181 |
182 | .CodeMirror-hints {
183 | margin: 0;
184 | padding: 0;
185 | position: absolute;
186 | z-index: 10;
187 | list-style: none;
188 | box-shadow: 2px 3px 5px rgba(0, 0, 0, .2);
189 | border: 1px solid silver;
190 | background: white;
191 | font-size: 90%;
192 | font-family: monospace;
193 | max-height: 30em;
194 | max-width: 600px;
195 | overflow-y: auto;
196 | overflow-x: auto;
197 | }
198 |
199 | .CodeMirror-hint {
200 | margin: 2px 0;
201 | padding: 0 4px;
202 | white-space: pre;
203 | color: #657b83;
204 | cursor: pointer;
205 | font-size: 11pt;
206 | background-position-x: 5px;
207 | }
208 |
209 | .CodeMirror-hint b {
210 | color: #073642;
211 | }
212 |
213 | .CodeMirror-hint-active {
214 | background-color: #EFEFF4;
215 | }
216 |
217 |
218 | /*
219 | .cm-hint-keyword {
220 | }
221 | */
222 |
223 | .cm-hint-label {
224 | padding-left: 22px !important;
225 | background-size: auto 80% !important;
226 | background-position: 3px center;
227 | background-repeat: no-repeat !important;
228 | background-image: url("data:image/svg+xml;utf8,");
229 | }
230 |
231 | .cm-hint-relationshipType {
232 | padding-left: 22px !important;
233 | background-size: auto 80% !important;
234 | background-position: 3px center;
235 | background-repeat: no-repeat !important;
236 | background-image: url("data:image/svg+xml;utf8,");
237 | }
238 |
239 | .cm-hint-variable {
240 | padding-left: 22px !important;
241 | background-size: auto 80% !important;
242 | background-position: 3px center;
243 | background-repeat: no-repeat !important;
244 | background-image: url("data:image/svg+xml;utf8,");
245 | }
246 |
247 | .cm-hint-procedure {
248 | padding-left: 22px !important;
249 | background-size: auto 80% !important;
250 | background-position: 3px center;
251 | background-repeat: no-repeat !important;
252 | background-image: url("data:image/svg+xml;utf8,");
253 | }
254 |
255 | .cm-hint-function {
256 | padding-left: 22px !important;
257 | background-size: auto 80% !important;
258 | background-position: 3px center;
259 | background-repeat: no-repeat !important;
260 | background-image: url("data:image/svg+xml;utf8,");
261 | }
262 |
263 | .cm-hint-parameter {
264 | padding-left: 22px !important;
265 | background-size: auto 80% !important;
266 | background-position: 3px center;
267 | background-repeat: no-repeat !important;
268 | background-image: url("data:image/svg+xml;utf8,");
269 | }
270 |
271 | .cm-hint-propertyKey {
272 | padding-left: 22px !important;
273 | background-size: auto 80% !important;
274 | background-position: 3px center;
275 | background-repeat: no-repeat !important;
276 | background-image: url("data:image/svg+xml;utf8,");
277 | }
278 |
279 | .cm-hint-consoleCommand {
280 | padding-left: 22px !important;
281 | background-size: auto 80% !important;
282 | background-position: 3px center;
283 | background-repeat: no-repeat !important;
284 | background-image: url("data:image/svg+xml;utf8,");
285 | }
286 |
287 | .cm-hint-consoleCommandSubcommand {
288 | padding-left: 22px !important;
289 | background-size: auto 80% !important;
290 | background-position: 3px center;
291 | background-repeat: no-repeat !important;
292 | background-image: url("data:image/svg+xml;utf8,");
293 | }
294 |
295 | .cm-hint-procedureOutput {
296 | padding-left: 22px !important;
297 | background-size: auto 80% !important;
298 | background-position: 3px center;
299 | background-repeat: no-repeat !important;
300 | background-image: url("data:image/svg+xml;utf8,");
301 | }
--------------------------------------------------------------------------------
/www/src/config.js:
--------------------------------------------------------------------------------
1 | import icons from './icons.js'
2 | import colors from 'vuetify/lib/util/colors'
3 |
4 | export const access = {
5 | "Allow": colors.green.base,
6 | "Deny": colors.red.base,
7 | "List": colors.yellow.base,
8 | "Permissions Management": colors.deepPurple.darken1,
9 | "Read": colors.pink.darken1,
10 | "Tagging": colors.teal.darken1,
11 | "Write": colors.indigo.darken2,
12 | }
13 |
14 | export const size = {
15 | "ACTION": 1,
16 | "ACTIONS": 1,
17 | "ADMIN": 3,
18 | "ATTACK": 1,
19 | "TRANSITIVE": 2,
20 | "TRUSTS": 1,
21 | 'ASSOCIATIVE': 1,
22 | }
23 |
24 | const cache = {}
25 | const badge = (n, m) => {
26 |
27 | if (!(Object.keys(cache).includes(m)))
28 | cache[m] = {}
29 |
30 | if (!(n.data().type in cache[m])) {
31 |
32 | let svg = decodeURIComponent(n.style("background-image")).split('