pangea-sdk 3.0.0__py3-none-any.whl → 3.2.0__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.
- pangea/__init__.py +1 -1
- pangea/asyncio/request.py +138 -22
- pangea/asyncio/services/audit.py +82 -3
- pangea/asyncio/services/file_scan.py +6 -2
- pangea/exceptions.py +20 -2
- pangea/request.py +158 -35
- pangea/response.py +26 -4
- pangea/services/audit/audit.py +163 -62
- pangea/services/audit/models.py +32 -0
- pangea/services/authn/models.py +2 -2
- pangea/services/file_scan.py +21 -3
- pangea/services/vault/models/common.py +11 -0
- pangea/utils.py +24 -1
- {pangea_sdk-3.0.0.dist-info → pangea_sdk-3.2.0.dist-info}/METADATA +2 -1
- {pangea_sdk-3.0.0.dist-info → pangea_sdk-3.2.0.dist-info}/RECORD +16 -16
- {pangea_sdk-3.0.0.dist-info → pangea_sdk-3.2.0.dist-info}/WHEEL +0 -0
pangea/response.py
CHANGED
@@ -12,6 +12,17 @@ from pydantic import BaseModel
|
|
12
12
|
T = TypeVar("T")
|
13
13
|
|
14
14
|
|
15
|
+
class TransferMethod(str, enum.Enum):
|
16
|
+
DIRECT = "direct"
|
17
|
+
MULTIPART = "multipart"
|
18
|
+
|
19
|
+
def __str__(self):
|
20
|
+
return str(self.value)
|
21
|
+
|
22
|
+
def __repr__(self):
|
23
|
+
return str(self.value)
|
24
|
+
|
25
|
+
|
15
26
|
# API response should accept arbitrary fields to make them accept possible new parameters
|
16
27
|
class APIResponseModel(BaseModel):
|
17
28
|
class Config:
|
@@ -59,6 +70,15 @@ class ErrorField(APIResponseModel):
|
|
59
70
|
return self.__repr__()
|
60
71
|
|
61
72
|
|
73
|
+
class AcceptedStatus(APIResponseModel):
|
74
|
+
upload_url: str = ""
|
75
|
+
upload_details: Dict[str, Any] = {}
|
76
|
+
|
77
|
+
|
78
|
+
class AcceptedResult(PangeaResponseResult):
|
79
|
+
accepted_status: Optional[AcceptedStatus] = None
|
80
|
+
|
81
|
+
|
62
82
|
class PangeaError(PangeaResponseResult):
|
63
83
|
errors: List[ErrorField] = []
|
64
84
|
|
@@ -109,6 +129,7 @@ class PangeaResponse(Generic[T], ResponseHeader):
|
|
109
129
|
raw_response: Optional[Union[requests.Response, aiohttp.ClientResponse]] = None
|
110
130
|
result: Optional[T] = None
|
111
131
|
pangea_error: Optional[PangeaError] = None
|
132
|
+
accepted_result: Optional[AcceptedResult] = None
|
112
133
|
result_class: Type[PangeaResponseResult] = PangeaResponseResult
|
113
134
|
_json: Any
|
114
135
|
|
@@ -120,13 +141,14 @@ class PangeaResponse(Generic[T], ResponseHeader):
|
|
120
141
|
self.result_class = result_class
|
121
142
|
self.result = (
|
122
143
|
self.result_class(**self.raw_result)
|
123
|
-
if self.raw_result is not None
|
124
|
-
and issubclass(self.result_class, PangeaResponseResult)
|
125
|
-
and self.status == ResponseStatus.SUCCESS.value
|
144
|
+
if self.raw_result is not None and issubclass(self.result_class, PangeaResponseResult) and self.success
|
126
145
|
else None
|
127
146
|
)
|
128
147
|
if not self.success:
|
129
|
-
|
148
|
+
if self.http_status == 202:
|
149
|
+
self.accepted_result = AcceptedResult(**self.raw_result) if self.raw_result is not None else None
|
150
|
+
else:
|
151
|
+
self.pangea_error = PangeaError(**self.raw_result) if self.raw_result is not None else None
|
130
152
|
|
131
153
|
@property
|
132
154
|
def success(self) -> bool:
|
pangea/services/audit/audit.py
CHANGED
@@ -2,14 +2,18 @@
|
|
2
2
|
# Author: Pangea Cyber Corporation
|
3
3
|
import datetime
|
4
4
|
import json
|
5
|
-
from typing import Any, Dict, Optional, Union
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
6
6
|
|
7
|
+
import pangea.exceptions as pexc
|
7
8
|
from pangea.response import PangeaResponse
|
8
9
|
from pangea.services.audit.exceptions import AuditException, EventCorruption
|
9
10
|
from pangea.services.audit.models import (
|
10
11
|
Event,
|
11
12
|
EventEnvelope,
|
12
13
|
EventVerification,
|
14
|
+
LogBulkRequest,
|
15
|
+
LogBulkResult,
|
16
|
+
LogEvent,
|
13
17
|
LogRequest,
|
14
18
|
LogResult,
|
15
19
|
PublishedRoot,
|
@@ -56,7 +60,29 @@ class AuditBase:
|
|
56
60
|
self.prev_unpublished_root_hash: Optional[str] = None
|
57
61
|
self.tenant_id = tenant_id
|
58
62
|
|
59
|
-
def
|
63
|
+
def _get_log_request(
|
64
|
+
self, events: Union[dict, List[dict]], sign_local: bool, verify: bool, verbose: Optional[bool]
|
65
|
+
) -> Union[LogRequest, LogBulkResult]:
|
66
|
+
if isinstance(events, list):
|
67
|
+
request_events: List[LogEvent] = []
|
68
|
+
for e in events:
|
69
|
+
request_events.append(self._process_log(e, sign_local=sign_local))
|
70
|
+
|
71
|
+
input = LogBulkRequest(events=request_events, verbose=verbose)
|
72
|
+
|
73
|
+
elif isinstance(events, dict):
|
74
|
+
log = self._process_log(events, sign_local=sign_local)
|
75
|
+
input = LogRequest(event=log.event, signature=log.signature, public_key=log.public_key)
|
76
|
+
input.verbose = True if verify else verbose
|
77
|
+
if verify and self.prev_unpublished_root_hash:
|
78
|
+
input.prev_root = self.prev_unpublished_root_hash
|
79
|
+
|
80
|
+
else:
|
81
|
+
raise AttributeError(f"events should be a dict or a list[dict] and it is {type(events)}")
|
82
|
+
|
83
|
+
return input
|
84
|
+
|
85
|
+
def _process_log(self, event: dict, sign_local: bool) -> LogEvent:
|
60
86
|
if event.get("tenant_id", None) is None and self.tenant_id:
|
61
87
|
event["tenant_id"] = self.tenant_id
|
62
88
|
|
@@ -66,70 +92,60 @@ class AuditBase:
|
|
66
92
|
if sign_local is True and self.signer is None:
|
67
93
|
raise AuditException("Error: the `signing` parameter set, but `signer` is not configured")
|
68
94
|
|
69
|
-
|
70
|
-
|
95
|
+
signature = None
|
96
|
+
pki = None
|
71
97
|
if sign_local is True:
|
72
98
|
data2sign = canonicalize_event(event)
|
73
99
|
signature = self.signer.sign(data2sign)
|
74
|
-
if signature is
|
75
|
-
input.signature = signature
|
76
|
-
else:
|
100
|
+
if signature is None:
|
77
101
|
raise AuditException("Error: failure signing message")
|
78
102
|
|
79
103
|
# Add public key value to public key info and serialize
|
80
|
-
self.
|
81
|
-
|
82
|
-
if verify:
|
83
|
-
input.verbose = True
|
84
|
-
if self.prev_unpublished_root_hash:
|
85
|
-
input.prev_root = self.prev_unpublished_root_hash
|
86
|
-
|
87
|
-
return input
|
104
|
+
pki = self._get_public_key_info(self.signer, self.public_key_info)
|
88
105
|
|
89
|
-
|
90
|
-
if not response.success:
|
91
|
-
return response
|
106
|
+
return LogEvent(event=event, signature=signature, public_key=pki)
|
92
107
|
|
93
|
-
|
108
|
+
def _process_log_result(self, result: LogResult, verify: bool):
|
109
|
+
new_unpublished_root_hash = result.unpublished_root
|
94
110
|
|
95
111
|
if verify:
|
96
|
-
if
|
112
|
+
if result.envelope:
|
97
113
|
# verify event hash
|
98
|
-
if
|
114
|
+
if result.hash and not verify_envelope_hash(result.envelope, result.hash):
|
99
115
|
# it's a extreme case, it's OK to raise an exception
|
100
|
-
raise EventCorruption("Error: Event hash failed.",
|
116
|
+
raise EventCorruption("Error: Event hash failed.", result.envelope)
|
101
117
|
|
102
|
-
|
118
|
+
result.signature_verification = self.verify_signature(result.envelope)
|
103
119
|
|
104
120
|
if new_unpublished_root_hash:
|
105
|
-
if
|
121
|
+
if result.membership_proof is not None:
|
106
122
|
# verify membership proofs
|
107
|
-
membership_proof = decode_membership_proof(
|
123
|
+
membership_proof = decode_membership_proof(result.membership_proof)
|
108
124
|
if verify_membership_proof(
|
109
|
-
node_hash=decode_hash(
|
125
|
+
node_hash=decode_hash(result.hash),
|
110
126
|
root_hash=decode_hash(new_unpublished_root_hash),
|
111
127
|
proof=membership_proof,
|
112
128
|
):
|
113
|
-
|
129
|
+
result.membership_verification = EventVerification.PASS
|
114
130
|
else:
|
115
|
-
|
131
|
+
result.membership_verification = EventVerification.FAIL
|
116
132
|
|
117
133
|
# verify consistency proofs (following events)
|
118
|
-
if
|
119
|
-
consistency_proof = decode_consistency_proof(
|
134
|
+
if result.consistency_proof is not None and self.prev_unpublished_root_hash:
|
135
|
+
consistency_proof = decode_consistency_proof(result.consistency_proof)
|
120
136
|
if verify_consistency_proof(
|
121
137
|
new_root=decode_hash(new_unpublished_root_hash),
|
122
138
|
prev_root=decode_hash(self.prev_unpublished_root_hash),
|
123
139
|
proof=consistency_proof,
|
124
140
|
):
|
125
|
-
|
141
|
+
result.consistency_verification = EventVerification.PASS
|
126
142
|
else:
|
127
|
-
|
143
|
+
result.consistency_verification = EventVerification.FAIL
|
128
144
|
|
129
145
|
# Update prev unpublished root
|
130
146
|
if new_unpublished_root_hash:
|
131
147
|
self.prev_unpublished_root_hash = new_unpublished_root_hash
|
132
|
-
return
|
148
|
+
return
|
133
149
|
|
134
150
|
def handle_results_response(
|
135
151
|
self, response: PangeaResponse[SearchResultOutput], verify_consistency: bool = False, verify_events: bool = True
|
@@ -348,12 +364,10 @@ class AuditBase:
|
|
348
364
|
else:
|
349
365
|
return EventVerification.NONE
|
350
366
|
|
351
|
-
def
|
367
|
+
def _get_public_key_info(self, signer: Signer, public_key_info: Dict[str, str]):
|
352
368
|
public_key_info["key"] = signer.get_public_key_PEM()
|
353
369
|
public_key_info["algorithm"] = signer.get_algorithm()
|
354
|
-
|
355
|
-
public_key_info, ensure_ascii=False, allow_nan=False, separators=(",", ":"), sort_keys=True
|
356
|
-
)
|
370
|
+
return json.dumps(public_key_info, ensure_ascii=False, allow_nan=False, separators=(",", ":"), sort_keys=True)
|
357
371
|
|
358
372
|
|
359
373
|
class Audit(ServiceBase, AuditBase):
|
@@ -444,16 +458,13 @@ class Audit(ServiceBase, AuditBase):
|
|
444
458
|
A PangeaResponse where the hash of event data and optional verbose
|
445
459
|
results are returned in the response.result field.
|
446
460
|
Available response fields can be found in our
|
447
|
-
[API documentation](https://pangea.cloud/docs/api/audit
|
461
|
+
[API documentation](https://pangea.cloud/docs/api/audit#/v1/log).
|
448
462
|
|
449
463
|
Examples:
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
print(f"Request Error: {e.response.summary}")
|
455
|
-
for err in e.errors:
|
456
|
-
print(f"\\t{err.detail} \\n")
|
464
|
+
log_response = audit.log(
|
465
|
+
message="hello world",
|
466
|
+
verbose=True,
|
467
|
+
)
|
457
468
|
"""
|
458
469
|
|
459
470
|
event = Event(
|
@@ -481,6 +492,7 @@ class Audit(ServiceBase, AuditBase):
|
|
481
492
|
Log an entry
|
482
493
|
|
483
494
|
Create a log entry in the Secure Audit Log.
|
495
|
+
|
484
496
|
Args:
|
485
497
|
event (dict[str, Any]): event to be logged
|
486
498
|
verify (bool, optional): True to verify logs consistency after response.
|
@@ -493,11 +505,11 @@ class Audit(ServiceBase, AuditBase):
|
|
493
505
|
Returns:
|
494
506
|
A PangeaResponse where the hash of event data and optional verbose
|
495
507
|
results are returned in the response.result field.
|
496
|
-
Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit
|
508
|
+
Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#/v1/log).
|
497
509
|
|
498
510
|
Examples:
|
499
511
|
try:
|
500
|
-
log_response = audit.log({"message"
|
512
|
+
log_response = audit.log({"message": "hello world"}, verbose=True)
|
501
513
|
print(f"Response. Hash: {log_response.result.hash}")
|
502
514
|
except pe.PangeaAPIException as e:
|
503
515
|
print(f"Request Error: {e.response.summary}")
|
@@ -505,9 +517,98 @@ class Audit(ServiceBase, AuditBase):
|
|
505
517
|
print(f"\\t{err.detail} \\n")
|
506
518
|
"""
|
507
519
|
|
508
|
-
input = self.
|
520
|
+
input = self._get_log_request(event, sign_local=sign_local, verify=verify, verbose=verbose)
|
509
521
|
response = self.request.post("v1/log", LogResult, data=input.dict(exclude_none=True))
|
510
|
-
|
522
|
+
if response.success:
|
523
|
+
self._process_log_result(response.result, verify=verify)
|
524
|
+
return response
|
525
|
+
|
526
|
+
def log_bulk(
|
527
|
+
self,
|
528
|
+
events: List[Dict[str, Any]],
|
529
|
+
sign_local: bool = False,
|
530
|
+
verbose: Optional[bool] = None,
|
531
|
+
) -> PangeaResponse[LogBulkResult]:
|
532
|
+
"""
|
533
|
+
Log multiple entries
|
534
|
+
|
535
|
+
Create multiple log entries in the Secure Audit Log.
|
536
|
+
|
537
|
+
OperationId: audit_post_v2_log
|
538
|
+
|
539
|
+
Args:
|
540
|
+
events (List[dict[str, Any]]): events to be logged
|
541
|
+
sign_local (bool, optional): True to sign event with local key.
|
542
|
+
verbose (bool, optional): True to get a more verbose response.
|
543
|
+
Raises:
|
544
|
+
AuditException: If an audit based api exception happens
|
545
|
+
PangeaAPIException: If an API Error happens
|
546
|
+
|
547
|
+
Returns:
|
548
|
+
A PangeaResponse where the hash of event data and optional verbose
|
549
|
+
results are returned in the response.result field.
|
550
|
+
Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#/v2/log).
|
551
|
+
|
552
|
+
Examples:
|
553
|
+
log_response = audit.log_bulk(
|
554
|
+
events=[{"message": "hello world"}],
|
555
|
+
verbose=True,
|
556
|
+
)
|
557
|
+
"""
|
558
|
+
|
559
|
+
input = self._get_log_request(events, sign_local=sign_local, verify=False, verbose=verbose)
|
560
|
+
response = self.request.post("v2/log", LogBulkResult, data=input.dict(exclude_none=True))
|
561
|
+
|
562
|
+
if response.success:
|
563
|
+
for result in response.result.results:
|
564
|
+
self._process_log_result(result, verify=True)
|
565
|
+
return response
|
566
|
+
|
567
|
+
def log_bulk_async(
|
568
|
+
self,
|
569
|
+
events: List[Dict[str, Any]],
|
570
|
+
sign_local: bool = False,
|
571
|
+
verbose: Optional[bool] = None,
|
572
|
+
) -> PangeaResponse[LogBulkResult]:
|
573
|
+
"""
|
574
|
+
Log multiple entries asynchronously
|
575
|
+
|
576
|
+
Asynchronously create multiple log entries in the Secure Audit Log.
|
577
|
+
|
578
|
+
Args:
|
579
|
+
events (List[dict[str, Any]]): events to be logged
|
580
|
+
sign_local (bool, optional): True to sign event with local key.
|
581
|
+
verbose (bool, optional): True to get a more verbose response.
|
582
|
+
Raises:
|
583
|
+
AuditException: If an audit based api exception happens
|
584
|
+
PangeaAPIException: If an API Error happens
|
585
|
+
|
586
|
+
Returns:
|
587
|
+
A PangeaResponse where the hash of event data and optional verbose
|
588
|
+
results are returned in the response.result field.
|
589
|
+
Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#/v2/log_async).
|
590
|
+
|
591
|
+
Examples:
|
592
|
+
log_response = audit.log_bulk_async(
|
593
|
+
events=[{"message": "hello world"}],
|
594
|
+
verbose=True,
|
595
|
+
)
|
596
|
+
"""
|
597
|
+
|
598
|
+
input = self._get_log_request(events, sign_local=sign_local, verify=False, verbose=verbose)
|
599
|
+
|
600
|
+
try:
|
601
|
+
# Calling to v2 methods will return always a 202.
|
602
|
+
response = self.request.post(
|
603
|
+
"v2/log_async", LogBulkResult, data=input.dict(exclude_none=True), poll_result=False
|
604
|
+
)
|
605
|
+
except pexc.AcceptedRequestException as e:
|
606
|
+
return e.response
|
607
|
+
|
608
|
+
if response.success:
|
609
|
+
for result in response.result.results:
|
610
|
+
self._process_log_result(result, verify=True)
|
611
|
+
return response
|
511
612
|
|
512
613
|
def search(
|
513
614
|
self,
|
@@ -558,11 +659,17 @@ class Audit(ServiceBase, AuditBase):
|
|
558
659
|
|
559
660
|
Returns:
|
560
661
|
A PangeaResponse[SearchOutput] where the first page of matched events is returned in the
|
561
|
-
response.result field. Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit
|
562
|
-
Pagination can be found in the [search results endpoint](https://pangea.cloud/docs/api/audit
|
662
|
+
response.result field. Available response fields can be found in our [API documentation](https://pangea.cloud/docs/api/audit#/v1/search).
|
663
|
+
Pagination can be found in the [search results endpoint](https://pangea.cloud/docs/api/audit#/v1/results).
|
563
664
|
|
564
665
|
Examples:
|
565
|
-
response
|
666
|
+
response = audit.search(
|
667
|
+
query="message:test",
|
668
|
+
search_restriction={'source': ["monitor"]},
|
669
|
+
limit=1,
|
670
|
+
verify_consistency=True,
|
671
|
+
verify_events=True,
|
672
|
+
)
|
566
673
|
"""
|
567
674
|
|
568
675
|
if verify_consistency:
|
@@ -609,17 +716,11 @@ class Audit(ServiceBase, AuditBase):
|
|
609
716
|
PangeaAPIException: If an API Error happens
|
610
717
|
|
611
718
|
Examples:
|
612
|
-
|
613
|
-
|
614
|
-
search_restriction={'source': ["monitor"]},
|
615
|
-
limit=100,
|
616
|
-
verify_consistency=True,
|
617
|
-
verify_events=True)
|
618
|
-
|
619
|
-
result_res: PangeaResponse[SearchResultsOutput] = audit.results(
|
620
|
-
id=search_res.result.id,
|
719
|
+
response = audit.results(
|
720
|
+
id="pas_sqilrhruwu54uggihqj3aie24wrctakr",
|
621
721
|
limit=10,
|
622
|
-
offset=0
|
722
|
+
offset=0,
|
723
|
+
)
|
623
724
|
"""
|
624
725
|
|
625
726
|
if limit <= 0:
|
pangea/services/audit/models.py
CHANGED
@@ -146,6 +146,34 @@ class LogRequest(APIRequestModel):
|
|
146
146
|
prev_root: Optional[str] = None
|
147
147
|
|
148
148
|
|
149
|
+
class LogEvent(APIRequestModel):
|
150
|
+
"""
|
151
|
+
Event to perform a log action
|
152
|
+
|
153
|
+
Arguments:
|
154
|
+
event -- A structured event describing an auditable activity.
|
155
|
+
signature -- An optional client-side signature for forgery protection.
|
156
|
+
public_key -- The base64-encoded ed25519 public key used for the signature, if one is provided.
|
157
|
+
"""
|
158
|
+
|
159
|
+
event: Dict[str, Any]
|
160
|
+
signature: Optional[str] = None
|
161
|
+
public_key: Optional[str] = None
|
162
|
+
|
163
|
+
|
164
|
+
class LogBulkRequest(APIRequestModel):
|
165
|
+
"""
|
166
|
+
Request to perform a bulk log action
|
167
|
+
|
168
|
+
Arguments:
|
169
|
+
events -- A list structured events describing an auditable activity.
|
170
|
+
verbose -- If true, be verbose in the response; include membership proof, unpublished root and consistency proof, etc.
|
171
|
+
"""
|
172
|
+
|
173
|
+
events: List[LogEvent]
|
174
|
+
verbose: Optional[bool] = None
|
175
|
+
|
176
|
+
|
149
177
|
class LogResult(PangeaResponseResult):
|
150
178
|
"""
|
151
179
|
Result class after an audit log action
|
@@ -167,6 +195,10 @@ class LogResult(PangeaResponseResult):
|
|
167
195
|
signature_verification: EventVerification = EventVerification.NONE
|
168
196
|
|
169
197
|
|
198
|
+
class LogBulkResult(PangeaResponseResult):
|
199
|
+
results: List[LogResult] = []
|
200
|
+
|
201
|
+
|
170
202
|
class SearchRestriction(APIResponseModel):
|
171
203
|
"""
|
172
204
|
Set of restrictions when perform an audit search action
|
pangea/services/authn/models.py
CHANGED
@@ -152,7 +152,7 @@ class UserListOrderBy(enum.Enum):
|
|
152
152
|
class Authenticator(APIResponseModel):
|
153
153
|
id: str
|
154
154
|
type: str
|
155
|
-
|
155
|
+
enabled: bool
|
156
156
|
provider: Optional[str] = None
|
157
157
|
rpid: Optional[str] = None
|
158
158
|
phase: Optional[str] = None
|
@@ -399,7 +399,7 @@ class UserAuthenticatorsListRequest(APIRequestModel):
|
|
399
399
|
|
400
400
|
|
401
401
|
class UserAuthenticatorsListResult(PangeaResponseResult):
|
402
|
-
authenticators: List[Authenticator]
|
402
|
+
authenticators: List[Authenticator] = []
|
403
403
|
|
404
404
|
|
405
405
|
class FlowCompleteRequest(APIRequestModel):
|
pangea/services/file_scan.py
CHANGED
@@ -3,7 +3,8 @@
|
|
3
3
|
import io
|
4
4
|
from typing import Dict, List, Optional
|
5
5
|
|
6
|
-
from pangea.response import APIRequestModel, PangeaResponse, PangeaResponseResult
|
6
|
+
from pangea.response import APIRequestModel, PangeaResponse, PangeaResponseResult, TransferMethod
|
7
|
+
from pangea.utils import get_presigned_url_upload_params
|
7
8
|
|
8
9
|
from .base import ServiceBase
|
9
10
|
|
@@ -20,6 +21,10 @@ class FileScanRequest(APIRequestModel):
|
|
20
21
|
verbose: Optional[bool] = None
|
21
22
|
raw: Optional[bool] = None
|
22
23
|
provider: Optional[str] = None
|
24
|
+
transfer_size: Optional[int] = None
|
25
|
+
transfer_crc32c: Optional[str] = None
|
26
|
+
transfer_sha256: Optional[str] = None
|
27
|
+
transfer_method: TransferMethod = TransferMethod.DIRECT
|
23
28
|
|
24
29
|
|
25
30
|
class FileScanData(PangeaResponseResult):
|
@@ -74,6 +79,7 @@ class FileScan(ServiceBase):
|
|
74
79
|
raw: Optional[bool] = None,
|
75
80
|
provider: Optional[str] = None,
|
76
81
|
sync_call: bool = True,
|
82
|
+
transfer_method: TransferMethod = TransferMethod.DIRECT,
|
77
83
|
) -> PangeaResponse[FileScanResult]:
|
78
84
|
"""
|
79
85
|
Scan
|
@@ -108,14 +114,26 @@ class FileScan(ServiceBase):
|
|
108
114
|
for err in e.errors:
|
109
115
|
print(f"\\t{err.detail} \\n")
|
110
116
|
"""
|
111
|
-
input = FileScanRequest(verbose=verbose, raw=raw, provider=provider)
|
112
117
|
|
113
118
|
if file or file_path:
|
114
119
|
if file_path:
|
115
120
|
file = open(file_path, "rb")
|
116
|
-
|
121
|
+
if transfer_method == TransferMethod.DIRECT:
|
122
|
+
crc, sha, size, _ = get_presigned_url_upload_params(file)
|
123
|
+
else:
|
124
|
+
crc, sha, size = None, None, None
|
125
|
+
files = [("upload", ("filename", file, "application/octet-stream"))]
|
117
126
|
else:
|
118
127
|
raise ValueError("Need to set file_path or file arguments")
|
119
128
|
|
129
|
+
input = FileScanRequest(
|
130
|
+
verbose=verbose,
|
131
|
+
raw=raw,
|
132
|
+
provider=provider,
|
133
|
+
transfer_crc32c=crc,
|
134
|
+
transfer_sha256=sha,
|
135
|
+
transfer_size=size,
|
136
|
+
transfer_method=transfer_method,
|
137
|
+
)
|
120
138
|
data = input.dict(exclude_none=True)
|
121
139
|
return self.request.post("v1/scan", FileScanResult, data=data, files=files, poll_result=sync_call)
|
@@ -129,6 +129,7 @@ class ItemVersionState(str, enum.Enum):
|
|
129
129
|
SUSPENDED = "suspended"
|
130
130
|
COMPROMISED = "compromised"
|
131
131
|
DESTROYED = "destroyed"
|
132
|
+
INHERITED = "inherited"
|
132
133
|
|
133
134
|
def __str__(self):
|
134
135
|
return str(self.value)
|
@@ -217,9 +218,16 @@ class ItemData(PangeaResponseResult):
|
|
217
218
|
purpose: Optional[str] = None
|
218
219
|
|
219
220
|
|
221
|
+
class InheritedSettings(PangeaResponseResult):
|
222
|
+
rotation_frequency: Optional[str] = None
|
223
|
+
rotation_state: Optional[str] = None
|
224
|
+
rotation_grace_period: Optional[str] = None
|
225
|
+
|
226
|
+
|
220
227
|
class GetResult(ItemData):
|
221
228
|
versions: List[ItemVersionData] = []
|
222
229
|
rotation_grace_period: Optional[str] = None
|
230
|
+
inherited_settings: Optional[InheritedSettings] = None
|
223
231
|
|
224
232
|
|
225
233
|
class ListItemData(ItemData):
|
@@ -360,6 +368,9 @@ class FolderCreateRequest(APIRequestModel):
|
|
360
368
|
folder: str
|
361
369
|
metadata: Optional[Metadata] = None
|
362
370
|
tags: Optional[Tags] = None
|
371
|
+
rotation_frequency: Optional[str] = None
|
372
|
+
rotation_state: Optional[ItemVersionState] = None
|
373
|
+
rotation_grace_period: Optional[str] = None
|
363
374
|
|
364
375
|
|
365
376
|
class FolderCreateResult(PangeaResponseResult):
|
pangea/utils.py
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
import base64
|
2
2
|
import copy
|
3
3
|
import datetime
|
4
|
+
import io
|
4
5
|
import json
|
5
6
|
from binascii import hexlify
|
6
7
|
from collections import OrderedDict
|
7
8
|
from hashlib import new, sha1, sha256, sha512
|
8
9
|
|
10
|
+
from google_crc32c import Checksum as CRC32C
|
11
|
+
|
9
12
|
|
10
13
|
def format_datetime(dt: datetime.datetime) -> str:
|
11
14
|
"""
|
@@ -22,7 +25,6 @@ def default_encoder(obj) -> str:
|
|
22
25
|
if isinstance(obj, datetime.date):
|
23
26
|
return str(obj)
|
24
27
|
if isinstance(obj, dict):
|
25
|
-
print("encoder canonicalize obj")
|
26
28
|
return canonicalize(obj)
|
27
29
|
else:
|
28
30
|
return str(obj)
|
@@ -96,3 +98,24 @@ def hash_ntlm(data: str):
|
|
96
98
|
|
97
99
|
def get_prefix(hash: str, len: int = 5):
|
98
100
|
return hash[0:len]
|
101
|
+
|
102
|
+
|
103
|
+
def get_presigned_url_upload_params(file: io.BufferedReader):
|
104
|
+
if "b" not in file.mode:
|
105
|
+
raise AttributeError("File need to be open in binary mode")
|
106
|
+
|
107
|
+
file.seek(0) # restart reading
|
108
|
+
crc = CRC32C()
|
109
|
+
size = 0
|
110
|
+
sha = sha256()
|
111
|
+
|
112
|
+
while True:
|
113
|
+
chunk = file.read(1024 * 1024)
|
114
|
+
if not chunk:
|
115
|
+
break
|
116
|
+
crc.update(chunk)
|
117
|
+
sha.update(chunk)
|
118
|
+
size += len(chunk)
|
119
|
+
|
120
|
+
file.seek(0) # restart reading
|
121
|
+
return crc.hexdigest().decode("utf-8"), sha.hexdigest(), size, file
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pangea-sdk
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.2.0
|
4
4
|
Summary: Pangea API SDK
|
5
5
|
License: MIT
|
6
6
|
Keywords: Pangea,SDK,Audit
|
@@ -20,6 +20,7 @@ Requires-Dist: alive-progress (>=2.4.1,<3.0.0)
|
|
20
20
|
Requires-Dist: asyncio (>=3.4.3,<4.0.0)
|
21
21
|
Requires-Dist: cryptography (==41.0.3)
|
22
22
|
Requires-Dist: deprecated (>=1.2.13,<2.0.0)
|
23
|
+
Requires-Dist: google-crc32c (>=1.5.0,<2.0.0)
|
23
24
|
Requires-Dist: pydantic (>=1.10.2,<2.0.0)
|
24
25
|
Requires-Dist: pytest (>=7.2.0,<8.0.0)
|
25
26
|
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|