praetorian-cli 2.2.2__py3-none-any.whl → 2.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- praetorian_cli/handlers/add.py +47 -7
- praetorian_cli/handlers/cli_decorators.py +1 -1
- praetorian_cli/handlers/delete.py +15 -2
- praetorian_cli/handlers/get.py +48 -2
- praetorian_cli/handlers/link.py +29 -1
- praetorian_cli/handlers/list.py +30 -9
- praetorian_cli/handlers/unlink.py +29 -1
- praetorian_cli/handlers/update.py +3 -3
- praetorian_cli/sdk/chariot.py +6 -12
- praetorian_cli/sdk/entities/assets.py +30 -12
- praetorian_cli/sdk/entities/schema.py +27 -0
- praetorian_cli/sdk/entities/seeds.py +108 -56
- praetorian_cli/sdk/entities/webpage.py +180 -0
- praetorian_cli/sdk/mcp_server.py +2 -3
- praetorian_cli/sdk/model/globals.py +2 -0
- praetorian_cli/sdk/model/query.py +8 -1
- praetorian_cli/sdk/model/utils.py +2 -8
- praetorian_cli/sdk/test/test_asset.py +38 -2
- praetorian_cli/sdk/test/test_seed.py +13 -14
- praetorian_cli/sdk/test/test_webpage.py +46 -0
- praetorian_cli/sdk/test/test_z_cli.py +53 -24
- praetorian_cli/sdk/test/utils.py +21 -4
- {praetorian_cli-2.2.2.dist-info → praetorian_cli-2.2.4.dist-info}/METADATA +1 -1
- {praetorian_cli-2.2.2.dist-info → praetorian_cli-2.2.4.dist-info}/RECORD +28 -25
- {praetorian_cli-2.2.2.dist-info → praetorian_cli-2.2.4.dist-info}/WHEEL +0 -0
- {praetorian_cli-2.2.2.dist-info → praetorian_cli-2.2.4.dist-info}/entry_points.txt +0 -0
- {praetorian_cli-2.2.2.dist-info → praetorian_cli-2.2.4.dist-info}/licenses/LICENSE +0 -0
- {praetorian_cli-2.2.2.dist-info → praetorian_cli-2.2.4.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from praetorian_cli.handlers.utils import error
|
|
2
2
|
from praetorian_cli.sdk.model.globals import Seed, Kind
|
|
3
|
+
from praetorian_cli.sdk.model.query import Query, Node, Filter, KIND_TO_LABEL
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class Seeds:
|
|
@@ -9,97 +10,148 @@ class Seeds:
|
|
|
9
10
|
def __init__(self, api):
|
|
10
11
|
self.api = api
|
|
11
12
|
|
|
12
|
-
def add(self,
|
|
13
|
+
def add(self, status=Seed.PENDING.value, seed_type=Kind.ASSET.value, **kwargs):
|
|
13
14
|
"""
|
|
14
|
-
Add a seed
|
|
15
|
-
|
|
16
|
-
:param
|
|
17
|
-
:type
|
|
18
|
-
:param
|
|
19
|
-
:type
|
|
15
|
+
Add a seed of specified type with dynamic fields.
|
|
16
|
+
|
|
17
|
+
:param status: Status for backward compatibility
|
|
18
|
+
:type status: str or None
|
|
19
|
+
:param type: Asset type (e.g., 'asset', 'addomain', etc.)
|
|
20
|
+
:type type: str
|
|
21
|
+
:param kwargs: Dynamic fields for the asset type
|
|
20
22
|
:return: The seed that was added
|
|
21
23
|
:rtype: dict
|
|
22
24
|
"""
|
|
23
|
-
|
|
25
|
+
# Handle status if provided
|
|
26
|
+
kwargs['status'] = status
|
|
27
|
+
|
|
28
|
+
# Build payload with type wrapper
|
|
29
|
+
payload = {
|
|
30
|
+
'type': seed_type,
|
|
31
|
+
'model': kwargs
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return self.api.upsert('seed', payload)
|
|
24
35
|
|
|
25
36
|
def get(self, key):
|
|
26
37
|
"""
|
|
27
38
|
Get details of a seed by key.
|
|
28
39
|
|
|
29
|
-
:param key: Entity key
|
|
40
|
+
:param key: Entity key (e.g., '#asset#example.com#example.com')
|
|
30
41
|
:type key: str
|
|
31
42
|
:return: The seed matching the specified key, or None if not found
|
|
32
43
|
:rtype: dict or None
|
|
33
44
|
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
|
|
46
|
+
# Create a Filter for the key field
|
|
47
|
+
key_filter = Filter(
|
|
48
|
+
field=Filter.Field.KEY,
|
|
49
|
+
operator=Filter.Operator.EQUAL,
|
|
50
|
+
value=key
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Create a Node with Seed label and key filter
|
|
54
|
+
node = Node(
|
|
55
|
+
labels=[Node.Label.SEED],
|
|
56
|
+
filters=[key_filter]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Create the Query object
|
|
60
|
+
query = Query(node=node)
|
|
61
|
+
|
|
62
|
+
# Call by_query with the constructed Query object
|
|
63
|
+
results_tuple = self.api.search.by_query(query)
|
|
64
|
+
if not results_tuple:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
results, _ = results_tuple
|
|
68
|
+
if len(results) == 0:
|
|
69
|
+
return None
|
|
70
|
+
return results[0]
|
|
71
|
+
|
|
72
|
+
def update(self, key, status=None):
|
|
37
73
|
"""
|
|
38
|
-
Update
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
internally uses the DNS of the original seed rather than the key for the update operation.
|
|
42
|
-
|
|
43
|
-
:param key: Entity key in format #seed#{type}#{dns} where type is 'domain', 'ip', or 'cidr' and dns is the seed value
|
|
74
|
+
Update seed fields dynamically.
|
|
75
|
+
|
|
76
|
+
:param key: Seed/Asset key (e.g., '#seed#domain#example.com' or '#asset#domain#example.com')
|
|
44
77
|
:type key: str
|
|
45
|
-
:param status:
|
|
46
|
-
:type status: str
|
|
78
|
+
:param status: Status for backward compatibility (can be positional)
|
|
79
|
+
:type status: str or None
|
|
80
|
+
:param kwargs: Fields to update
|
|
47
81
|
:return: The updated seed, or None if the seed was not found
|
|
48
82
|
:rtype: dict or None
|
|
49
83
|
"""
|
|
50
|
-
|
|
84
|
+
|
|
85
|
+
seed = self.get(key) # This already handles old key format conversion
|
|
51
86
|
if seed:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
87
|
+
update_payload = {
|
|
88
|
+
'key': key,
|
|
89
|
+
'status': status
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return self.api.upsert('seed', update_payload)
|
|
57
93
|
else:
|
|
58
|
-
error(f'Seed {key}
|
|
94
|
+
error(f'Seed {key} not found.')
|
|
59
95
|
|
|
60
96
|
def delete(self, key):
|
|
61
97
|
"""
|
|
62
|
-
Delete
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
it sets the seed's status to DELETED ('D'), which marks it as deleted while
|
|
66
|
-
preserving the record for audit purposes.
|
|
67
|
-
|
|
68
|
-
:param key: Entity key in format #seed#{type}#{dns} where type is 'domain', 'ip', or 'cidr' and dns is the seed value
|
|
98
|
+
Delete seed (supports both old and new key formats).
|
|
99
|
+
|
|
100
|
+
:param key: Seed/Asset key (e.g., '#asset#domain#example.com')
|
|
69
101
|
:type key: str
|
|
70
102
|
:return: The seed that was marked as deleted, or None if the seed was not found
|
|
71
103
|
:rtype: dict or None
|
|
72
104
|
"""
|
|
73
|
-
seed = self.
|
|
105
|
+
seed = self.get(key) # This already handles old key format conversion
|
|
106
|
+
|
|
74
107
|
if seed:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
delete_payload = {
|
|
109
|
+
'key': key,
|
|
110
|
+
'status': Seed.DELETED.value
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return self.api.upsert('seed', delete_payload)
|
|
78
114
|
else:
|
|
79
|
-
error(f'Seed {key}
|
|
115
|
+
error(f'Seed {key} not found.')
|
|
80
116
|
|
|
81
|
-
def list(self,
|
|
117
|
+
def list(self, seed_type=Kind.SEED.value, key_prefix='', pages=100000) -> tuple:
|
|
82
118
|
"""
|
|
83
|
-
List seeds with
|
|
84
|
-
|
|
85
|
-
:param
|
|
86
|
-
:
|
|
87
|
-
:param
|
|
88
|
-
:
|
|
89
|
-
:param offset: The offset of the page you want to retrieve results. If this is not supplied, this function retrieves from the first page
|
|
90
|
-
:type offset: str or None
|
|
119
|
+
List seeds by querying assets with 'Seed' label.
|
|
120
|
+
|
|
121
|
+
:param seed_type: Optional asset seed_type filter (e.g., 'asset', 'addomain')
|
|
122
|
+
:seed_type seed_type: str or None
|
|
123
|
+
:param key_prefix: Filter by key prefix
|
|
124
|
+
:seed_type key_prefix: str
|
|
91
125
|
:param pages: The number of pages of results to retrieve. <mcp>Start with one page of results unless specifically requested.</mcp>
|
|
92
|
-
:
|
|
126
|
+
:seed_type pages: int
|
|
93
127
|
:return: A tuple containing (list of seeds, next page offset)
|
|
94
|
-
:
|
|
128
|
+
:rseed_type: tuple
|
|
95
129
|
"""
|
|
96
|
-
prefix_term = '#seed#'
|
|
97
|
-
if type:
|
|
98
|
-
prefix_term = f'{prefix_term}{type}#'
|
|
99
|
-
if prefix_filter:
|
|
100
|
-
prefix_term = f'{prefix_term}{prefix_filter}'
|
|
101
130
|
|
|
102
|
-
|
|
131
|
+
if seed_type in KIND_TO_LABEL:
|
|
132
|
+
seed_type = KIND_TO_LABEL[seed_type]
|
|
133
|
+
elif not seed_type:
|
|
134
|
+
seed_type = Node.Label.SEED
|
|
135
|
+
else:
|
|
136
|
+
raise ValueError(f'Invalid seed type: {seed_type}')
|
|
137
|
+
|
|
138
|
+
node = Node(
|
|
139
|
+
labels=[seed_type],
|
|
140
|
+
filters=[]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
key_filter = Filter(
|
|
144
|
+
field=Filter.Field.KEY,
|
|
145
|
+
operator=Filter.Operator.STARTS_WITH,
|
|
146
|
+
value=key_prefix
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if key_prefix:
|
|
150
|
+
node.filters.append(key_filter)
|
|
151
|
+
|
|
152
|
+
query = Query(node=node)
|
|
153
|
+
|
|
154
|
+
return self.api.search.by_query(query, pages)
|
|
103
155
|
|
|
104
156
|
def attributes(self, key):
|
|
105
157
|
"""
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from praetorian_cli.sdk.model.query import Query, Node, Filter, Relationship
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Webpage:
|
|
5
|
+
"""The methods in this class are to be accessed from sdk.webpage, where sdk
|
|
6
|
+
is an instance of Chariot."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, api):
|
|
9
|
+
self.api = api
|
|
10
|
+
|
|
11
|
+
def add(self, url, parent_key=None):
|
|
12
|
+
"""
|
|
13
|
+
Add a Webpage to the Chariot database.
|
|
14
|
+
|
|
15
|
+
WebPages represent individual pages or endpoints that can be optionally
|
|
16
|
+
associated with a parent WebApplication. The backend uses a WebpageRequest
|
|
17
|
+
structure with embedded webpage data and optional parent key.
|
|
18
|
+
|
|
19
|
+
:param url: The full URL of the page
|
|
20
|
+
:type url: str
|
|
21
|
+
:param parent_key: Optional key of the parent WebApplication
|
|
22
|
+
:type webapp_key: str or None
|
|
23
|
+
:return: The created WebPage object
|
|
24
|
+
:rtype: dict
|
|
25
|
+
:raises Exception: If the URL is invalid or the request fails
|
|
26
|
+
|
|
27
|
+
**Example Usage:**
|
|
28
|
+
>>> # Add a simple page without parent
|
|
29
|
+
>>> page = sdk.webpage.add("https://app.example.com/login")
|
|
30
|
+
|
|
31
|
+
>>> # Add a page with parent WebApplication
|
|
32
|
+
>>> page = sdk.webpage.add(
|
|
33
|
+
... url="https://app.example.com/admin",
|
|
34
|
+
... parent_key="#webapplication#https://app.example.com")
|
|
35
|
+
|
|
36
|
+
**WebPage Object Structure:**
|
|
37
|
+
The returned Webpage object contains:
|
|
38
|
+
- key: Webpage identifier in format #webpage#{url}
|
|
39
|
+
- url: Full URL of the page
|
|
40
|
+
- status: Current status
|
|
41
|
+
- parent: Parent WebApplication relationship (if applicable)
|
|
42
|
+
- created: Creation timestamp
|
|
43
|
+
"""
|
|
44
|
+
if not url:
|
|
45
|
+
raise Exception("URL is required for Webpage")
|
|
46
|
+
|
|
47
|
+
if parent_key and not parent_key.startswith('#webapplication#'):
|
|
48
|
+
raise Exception("Invalid WebApplication key format")
|
|
49
|
+
|
|
50
|
+
payload = {
|
|
51
|
+
'webpage': {
|
|
52
|
+
'url': url,
|
|
53
|
+
'status': 'A' # Active status
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if parent_key:
|
|
59
|
+
payload['parent_key'] = parent_key
|
|
60
|
+
|
|
61
|
+
return self.api.post('webpage', payload)
|
|
62
|
+
|
|
63
|
+
def get(self, key):
|
|
64
|
+
"""
|
|
65
|
+
Get details of a specific Webpage by its key.
|
|
66
|
+
|
|
67
|
+
:param key: The WebPage key identifier
|
|
68
|
+
:type key: str
|
|
69
|
+
:return: Webpage object with detailed information, or None if not found
|
|
70
|
+
:rtype: dict or None
|
|
71
|
+
|
|
72
|
+
**Example Usage:**
|
|
73
|
+
>>> # Get a specific Webpage
|
|
74
|
+
>>> page = sdk.webpage.get("webpage_key_123")
|
|
75
|
+
|
|
76
|
+
**Webpage Object Structure:**
|
|
77
|
+
The returned Webpage object contains:
|
|
78
|
+
- key: Webpage identifier
|
|
79
|
+
- url: Full URL of the page
|
|
80
|
+
- created: Creation timestamp
|
|
81
|
+
- updated: Last update timestamp
|
|
82
|
+
"""
|
|
83
|
+
query = Query(node=Node(labels=[Node.Label.WEBPAGE], filters=[Filter(field=Filter.Field.KEY, operator=Filter.Operator.EQUAL, value=key)]))
|
|
84
|
+
return self.api.search.by_query(query)[0][0]
|
|
85
|
+
|
|
86
|
+
def list(self, parent_key=None, filter=None, offset=0, pages=100000) -> tuple:
|
|
87
|
+
"""
|
|
88
|
+
List Webpages, optionally filtered by parent WebApplication.
|
|
89
|
+
|
|
90
|
+
Retrieve Webpage entities with optional filtering capabilities. Can filter by
|
|
91
|
+
parent WebApplication.
|
|
92
|
+
|
|
93
|
+
:param parent_key: Filter pages by specific WebApplication (optional)
|
|
94
|
+
:type parent_key: str or None
|
|
95
|
+
:param filter: Filter pages by specific URL (optional)
|
|
96
|
+
:type filter: str or None
|
|
97
|
+
:param offset: The offset for pagination to retrieve a specific page of results
|
|
98
|
+
:type offset: str or None
|
|
99
|
+
:param pages: Maximum number of pages to retrieve (default: 100000 for all results)
|
|
100
|
+
:type pages: int
|
|
101
|
+
:return: A tuple containing (list of matching Webpages, next page offset)
|
|
102
|
+
:rtype: tuple
|
|
103
|
+
|
|
104
|
+
**Example Usage:**
|
|
105
|
+
>>> # List all WebPages
|
|
106
|
+
>>> pages, offset = sdk.webpage.list()
|
|
107
|
+
|
|
108
|
+
>>> # List pages for specific WebApplication
|
|
109
|
+
>>> pages, offset = sdk.webpage.list(
|
|
110
|
+
... webapp_key="#asset#webapp#https://app.example.com#https://app.example.com")
|
|
111
|
+
|
|
112
|
+
**WebPage Filtering:**
|
|
113
|
+
- parent_key: Filters by parent WebApplication
|
|
114
|
+
- filter: Filters by specific URL
|
|
115
|
+
"""
|
|
116
|
+
if parent_key and not parent_key.startswith('#webapplication#'):
|
|
117
|
+
raise Exception("Invalid WebApplication key format")
|
|
118
|
+
|
|
119
|
+
relationships = []
|
|
120
|
+
filters = []
|
|
121
|
+
if parent_key:
|
|
122
|
+
parentFilter = Filter(field=Filter.Field.KEY, operator=Filter.Operator.EQUAL, value=parent_key)
|
|
123
|
+
relationship = Relationship(label=Relationship.Label.HAS_WEBPAGE, target=Node(labels=[Node.Label.WEBAPPLICATION], filters=[parentFilter]))
|
|
124
|
+
relationships.append(relationship)
|
|
125
|
+
if filter:
|
|
126
|
+
urlFilter = Filter(field=Filter.Field.KEY, operator=Filter.Operator.CONTAINS, value=filter)
|
|
127
|
+
filters.append(urlFilter)
|
|
128
|
+
node = Node(labels=[Node.Label.WEBPAGE], filters=filters, relationships=relationships)
|
|
129
|
+
query = Query(node=node, page=offset, limit=pages)
|
|
130
|
+
return self.api.search.by_query(query, pages)
|
|
131
|
+
|
|
132
|
+
def delete(self, key):
|
|
133
|
+
"""
|
|
134
|
+
Delete a webpage by its key.
|
|
135
|
+
|
|
136
|
+
:param key: The WebPage key identifier
|
|
137
|
+
:type key: str
|
|
138
|
+
"""
|
|
139
|
+
body = {
|
|
140
|
+
'webpage': {
|
|
141
|
+
'key': key
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
self.api.delete('webpage', params={}, body=body)
|
|
145
|
+
|
|
146
|
+
def link_source(self, webpage_key, entity_key):
|
|
147
|
+
"""
|
|
148
|
+
Link a file or repository to a webpage as source code.
|
|
149
|
+
|
|
150
|
+
:param webpage_key: The webpage key in format #webpage#{url}
|
|
151
|
+
:type webpage_key: str
|
|
152
|
+
:param entity_key: The entity key (file or repository) to link. Format: #file#{path} or #repository#{url}#{name}
|
|
153
|
+
:type entity_key: str
|
|
154
|
+
:return: The updated webpage with linked artifacts
|
|
155
|
+
:rtype: dict
|
|
156
|
+
"""
|
|
157
|
+
data = {
|
|
158
|
+
'webpageKey': webpage_key,
|
|
159
|
+
'entityKey': entity_key
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return self.api.put('webpage/link', data, {})
|
|
163
|
+
|
|
164
|
+
def unlink_source(self, webpage_key, entity_key):
|
|
165
|
+
"""
|
|
166
|
+
Unlink a file or repository from a webpage's source code.
|
|
167
|
+
|
|
168
|
+
:param webpage_key: The webpage key in format #webpage#{url}
|
|
169
|
+
:type webpage_key: str
|
|
170
|
+
:param entity_key: The entity key (file or repository) to unlink. Format: #file#{path} or #repository#{url}#{name}
|
|
171
|
+
:type entity_key: str
|
|
172
|
+
:return: The updated webpage with artifacts removed
|
|
173
|
+
:rtype: dict
|
|
174
|
+
"""
|
|
175
|
+
data = {
|
|
176
|
+
'webpageKey': webpage_key,
|
|
177
|
+
'entityKey': entity_key
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return self.api.delete('webpage/link', data, {})
|
praetorian_cli/sdk/mcp_server.py
CHANGED
|
@@ -8,7 +8,6 @@ from mcp.server.lowlevel import Server
|
|
|
8
8
|
from mcp.server.stdio import stdio_server
|
|
9
9
|
from mcp.types import Tool, TextContent
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
class MCPServer:
|
|
13
12
|
def __init__(self, chariot_instance, allowable_tools: Optional[List[str]] = None):
|
|
14
13
|
self.chariot = chariot_instance
|
|
@@ -157,7 +156,7 @@ class MCPServer:
|
|
|
157
156
|
tools = []
|
|
158
157
|
for tool_name, tool_info in self.discovered_tools.items():
|
|
159
158
|
parameters = self._extract_parameters_from_doc(tool_info['doc'], tool_info['signature'])
|
|
160
|
-
|
|
159
|
+
|
|
161
160
|
properties = {}
|
|
162
161
|
required = []
|
|
163
162
|
|
|
@@ -170,7 +169,7 @@ class MCPServer:
|
|
|
170
169
|
"description": param_info["description"]
|
|
171
170
|
}
|
|
172
171
|
|
|
173
|
-
if param_info
|
|
172
|
+
if param_info.get("required", False):
|
|
174
173
|
required.append(param_name)
|
|
175
174
|
|
|
176
175
|
tool_schema = {
|
|
@@ -49,6 +49,8 @@ class Filter:
|
|
|
49
49
|
KEV = 'kev'
|
|
50
50
|
EXPLOIT = 'exploit'
|
|
51
51
|
PRIVATE = 'private'
|
|
52
|
+
PRIMARY_URL = 'primary_url'
|
|
53
|
+
URL = 'url'
|
|
52
54
|
|
|
53
55
|
def __init__(self, field: Field, operator: Operator, value: str, not_: bool = False):
|
|
54
56
|
self.field = field
|
|
@@ -65,6 +67,7 @@ class Relationship:
|
|
|
65
67
|
HAS_VULNERABILITY = 'HAS_VULNERABILITY'
|
|
66
68
|
DISCOVERED = 'DISCOVERED'
|
|
67
69
|
HAS_ATTRIBUTE = 'HAS_ATTRIBUTE'
|
|
70
|
+
HAS_WEBPAGE = 'HAS_WEBPAGE'
|
|
68
71
|
|
|
69
72
|
def __init__(self, label: Label, source: 'Node' = None, target: 'Node' = None, optional: bool = False, length: int = 0):
|
|
70
73
|
self.label = label
|
|
@@ -91,12 +94,14 @@ class Node:
|
|
|
91
94
|
ASSET = 'Asset'
|
|
92
95
|
REPOSITORY = 'Repository'
|
|
93
96
|
INTEGRATION = 'Integration'
|
|
94
|
-
ADDOMAIN = '
|
|
97
|
+
ADDOMAIN = 'ADDomain'
|
|
95
98
|
ATTRIBUTE = 'Attribute'
|
|
96
99
|
RISK = 'Risk'
|
|
97
100
|
PRESEED = 'Preseed'
|
|
98
101
|
SEED = 'Seed'
|
|
99
102
|
TTL = 'TTL'
|
|
103
|
+
WEBAPPLICATION = 'WebApplication'
|
|
104
|
+
WEBPAGE = 'Webpage'
|
|
100
105
|
|
|
101
106
|
def __init__(self, labels: list[Label] = None, filters: list[Filter] = None,
|
|
102
107
|
relationships: list[Relationship] = None):
|
|
@@ -157,6 +162,8 @@ KIND_TO_LABEL = {
|
|
|
157
162
|
Kind.REPOSITORY.value: Node.Label.REPOSITORY,
|
|
158
163
|
Kind.INTEGRATION.value: Node.Label.INTEGRATION,
|
|
159
164
|
Kind.ADDOMAIN.value: Node.Label.ADDOMAIN,
|
|
165
|
+
Kind.WEBAPPLICATION.value: Node.Label.WEBAPPLICATION,
|
|
166
|
+
Kind.WEBPAGE.value: Node.Label.WEBPAGE,
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
|
|
@@ -15,14 +15,11 @@ def integration_key(dns, name):
|
|
|
15
15
|
def risk_key(dns, name):
|
|
16
16
|
return f'#risk#{dns}#{name}'
|
|
17
17
|
|
|
18
|
-
|
|
19
18
|
def attribute_key(name, value, source_key):
|
|
20
19
|
return f'#attribute#{name}#{value}{source_key}'
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return f'#seed#{type}#{dns}'
|
|
25
|
-
|
|
21
|
+
def seed_asset_key(dns):
|
|
22
|
+
return f'#asset#{dns}#{dns}'
|
|
26
23
|
|
|
27
24
|
def preseed_key(type, title, value):
|
|
28
25
|
return f'#preseed#{type}#{title}#{value}'
|
|
@@ -32,6 +29,3 @@ def setting_key(name):
|
|
|
32
29
|
|
|
33
30
|
def configuration_key(name):
|
|
34
31
|
return f'#configuration#{name}'
|
|
35
|
-
|
|
36
|
-
def seed_status(type, status_code):
|
|
37
|
-
return f'{type}#{status_code}'
|
|
@@ -42,7 +42,7 @@ class TestAsset:
|
|
|
42
42
|
assert any([a['group'] == self.asset_dns for a in deleted_assets])
|
|
43
43
|
|
|
44
44
|
def test_add_ad_domain(self):
|
|
45
|
-
asset = self.sdk.assets.add(self.ad_domain_name, self.
|
|
45
|
+
asset = self.sdk.assets.add(self.ad_domain_name, self.ad_object_id, status=Asset.ACTIVE.value, surface='test-surface', type=Kind.ADDOMAIN.value)
|
|
46
46
|
assert asset['key'] == self.ad_domain_key
|
|
47
47
|
assert len(asset['attackSurface']) == 1
|
|
48
48
|
assert 'test-surface' in asset['attackSurface']
|
|
@@ -51,7 +51,7 @@ class TestAsset:
|
|
|
51
51
|
def test_get_ad_domain(self):
|
|
52
52
|
asset = self.sdk.assets.get(self.ad_domain_key)
|
|
53
53
|
assert asset['key'] == self.ad_domain_key
|
|
54
|
-
assert asset['
|
|
54
|
+
assert asset['domain'] == self.ad_domain_name
|
|
55
55
|
assert asset['status'] == Asset.ACTIVE.value
|
|
56
56
|
|
|
57
57
|
def test_list_ad_domain(self):
|
|
@@ -71,11 +71,47 @@ class TestAsset:
|
|
|
71
71
|
deleted_assets, _ = self.sdk.search.by_status(Asset.DELETED.value, Kind.ADDOMAIN.value)
|
|
72
72
|
assert any([a['key'] == self.ad_domain_key for a in deleted_assets])
|
|
73
73
|
|
|
74
|
+
def test_add_webapplication(self):
|
|
75
|
+
asset = self.sdk.assets.add(self.webapp_name, self.webapp_url, status=Asset.ACTIVE.value, surface='test-surface', type=Kind.WEBAPPLICATION.value)
|
|
76
|
+
assert asset['key'] == self.webapp_key
|
|
77
|
+
assert len(asset['attackSurface']) == 1
|
|
78
|
+
assert 'test-surface' in asset['attackSurface']
|
|
79
|
+
assert asset['status'] == Asset.ACTIVE.value
|
|
80
|
+
|
|
81
|
+
def test_get_webapplication(self):
|
|
82
|
+
asset = self.sdk.assets.get(self.webapp_key)
|
|
83
|
+
assert asset['key'] == self.webapp_key
|
|
84
|
+
assert asset['group'] == self.webapp_name
|
|
85
|
+
assert asset['identifier'] == self.webapp_url
|
|
86
|
+
assert asset['status'] == Asset.ACTIVE.value
|
|
87
|
+
|
|
88
|
+
def test_list_webapplication(self):
|
|
89
|
+
results, _ = self.sdk.assets.list(asset_type=Kind.WEBAPPLICATION.value)
|
|
90
|
+
assert len(results) > 0
|
|
91
|
+
assert any([a['key'] == self.webapp_key for a in results])
|
|
92
|
+
assert any([a['group'] == self.webapp_name for a in results])
|
|
93
|
+
assert any([a['identifier'] == self.webapp_url for a in results])
|
|
94
|
+
|
|
95
|
+
def test_update_webapplication(self):
|
|
96
|
+
self.sdk.assets.update(self.webapp_key, status=Asset.FROZEN.value, surface='abc')
|
|
97
|
+
asset = self.get_webapplication()
|
|
98
|
+
assert asset['status'] == Asset.FROZEN.value
|
|
99
|
+
assert 'abc' in asset['attackSurface']
|
|
100
|
+
|
|
101
|
+
def test_delete_webapplication(self):
|
|
102
|
+
self.sdk.assets.delete(self.webapp_key)
|
|
103
|
+
assert self.get_webapplication()['status'] == Asset.DELETED.value
|
|
104
|
+
deleted_assets, _ = self.sdk.search.by_status(Asset.DELETED.value, Kind.WEBAPPLICATION.value)
|
|
105
|
+
assert any([a['key'] == self.webapp_key for a in deleted_assets])
|
|
106
|
+
|
|
74
107
|
def get_asset(self):
|
|
75
108
|
return self.sdk.assets.get(self.asset_key)
|
|
76
109
|
|
|
77
110
|
def get_ad_domain(self):
|
|
78
111
|
return self.sdk.assets.get(self.ad_domain_key)
|
|
79
112
|
|
|
113
|
+
def get_webapplication(self):
|
|
114
|
+
return self.sdk.assets.get(self.webapp_key)
|
|
115
|
+
|
|
80
116
|
def teardown_class(self):
|
|
81
117
|
clean_test_entities(self.sdk, self)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
|
-
from praetorian_cli.sdk.model.globals import Seed
|
|
4
|
-
from praetorian_cli.sdk.model.utils import seed_status
|
|
3
|
+
from praetorian_cli.sdk.model.globals import Seed, Kind
|
|
5
4
|
from praetorian_cli.sdk.test.utils import make_test_values, clean_test_entities, setup_chariot
|
|
6
5
|
|
|
7
6
|
|
|
@@ -12,30 +11,30 @@ class TestSeed:
|
|
|
12
11
|
self.sdk = setup_chariot()
|
|
13
12
|
make_test_values(self)
|
|
14
13
|
|
|
15
|
-
def
|
|
16
|
-
seed = self.sdk.seeds.add(self.
|
|
17
|
-
assert seed['key'] == self.
|
|
14
|
+
def test_add_asset_seed(self):
|
|
15
|
+
seed = self.sdk.seeds.add(dns=self.seed_asset_dns)
|
|
16
|
+
assert seed['key'] == self.seed_asset_key
|
|
18
17
|
|
|
19
18
|
def test_get_seed(self):
|
|
20
19
|
a = self.get_seed()
|
|
21
|
-
assert a['dns'] == self.
|
|
22
|
-
assert a['status'] ==
|
|
20
|
+
assert a['dns'] == self.seed_asset_dns
|
|
21
|
+
assert a['status'] == Seed.PENDING.value
|
|
23
22
|
|
|
24
23
|
def test_list_seed(self):
|
|
25
|
-
results, _ = self.sdk.seeds.list(
|
|
24
|
+
results, _ = self.sdk.seeds.list(Kind.ASSET.value, f"#asset#{self.seed_asset_dns}")
|
|
26
25
|
assert len(results) == 1
|
|
27
|
-
assert results[0]['dns'] == self.
|
|
26
|
+
assert results[0]['dns'] == self.seed_asset_dns
|
|
28
27
|
|
|
29
28
|
def test_update_seed(self):
|
|
30
|
-
self.sdk.seeds.update(self.
|
|
31
|
-
assert self.get_seed()['status'] ==
|
|
29
|
+
self.sdk.seeds.update(self.seed_asset_key, Seed.ACTIVE.value)
|
|
30
|
+
assert self.get_seed()['status'] == Seed.ACTIVE.value
|
|
32
31
|
|
|
33
32
|
def test_delete_seed(self):
|
|
34
|
-
self.sdk.seeds.delete(self.
|
|
35
|
-
assert self.sdk.seeds.get(self.
|
|
33
|
+
self.sdk.seeds.delete(self.seed_asset_key)
|
|
34
|
+
assert self.sdk.seeds.get(self.seed_asset_key)['status'] == Seed.DELETED.value
|
|
36
35
|
|
|
37
36
|
def get_seed(self):
|
|
38
|
-
return self.sdk.seeds.get(self.
|
|
37
|
+
return self.sdk.seeds.get(self.seed_asset_key)
|
|
39
38
|
|
|
40
39
|
def teardown_class(self):
|
|
41
40
|
clean_test_entities(self.sdk, self)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from praetorian_cli.sdk.test.utils import make_test_values, setup_chariot
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.coherence
|
|
7
|
+
class TestWebpage:
|
|
8
|
+
"""Test suite for the Webpage entity class."""
|
|
9
|
+
|
|
10
|
+
def setup_class(self):
|
|
11
|
+
self.sdk = setup_chariot()
|
|
12
|
+
make_test_values(self)
|
|
13
|
+
|
|
14
|
+
def test_add_webpage(self):
|
|
15
|
+
"""Test adding a Webpage with URL provided."""
|
|
16
|
+
result = self.sdk.webpage.add(self.webpage_url)
|
|
17
|
+
|
|
18
|
+
assert result is not None
|
|
19
|
+
webpage = result.get('webpages')[0]
|
|
20
|
+
assert webpage.get('key') == self.webpage_key
|
|
21
|
+
assert webpage.get('url') == self.webpage_url
|
|
22
|
+
|
|
23
|
+
def test_get_webpage(self):
|
|
24
|
+
"""Test retrieving a Webpage by key."""
|
|
25
|
+
result = self.sdk.webpage.get(self.webpage_key)
|
|
26
|
+
assert result is not None
|
|
27
|
+
assert result.get('key') == self.webpage_key
|
|
28
|
+
assert result.get('url') == self.webpage_url
|
|
29
|
+
|
|
30
|
+
def test_list_webpages(self):
|
|
31
|
+
"""Test listing Webpages."""
|
|
32
|
+
results, offset = self.sdk.webpage.list(filter=self.webpage_url[:len(self.webpage_url)//2])
|
|
33
|
+
assert isinstance(results, list)
|
|
34
|
+
assert len(results) > 0
|
|
35
|
+
assert any(r.get('key') == self.webpage_key for r in results)
|
|
36
|
+
assert any(r.get('url') == self.webpage_url for r in results)
|
|
37
|
+
|
|
38
|
+
def test_add_webpage_empty_url_raises_exception(self):
|
|
39
|
+
"""Test that adding a Webpage with empty URL raises an exception."""
|
|
40
|
+
with pytest.raises(Exception, match="URL is required for Webpage"):
|
|
41
|
+
self.sdk.webpage.add("")
|
|
42
|
+
|
|
43
|
+
def test_add_webpage_none_url_raises_exception(self):
|
|
44
|
+
"""Test that adding a Webpage with None URL raises an exception."""
|
|
45
|
+
with pytest.raises(Exception, match="URL is required for Webpage"):
|
|
46
|
+
self.sdk.webpage.add(None)
|