pylookyloo 1.24.0__tar.gz → 1.26.1__tar.gz

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.

Potentially problematic release.


This version of pylookyloo might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pylookyloo
3
- Version: 1.24.0
3
+ Version: 1.26.1
4
4
  Summary: Python CLI and module for Lookyloo
5
5
  Home-page: https://github.com/lookyloo/PyLookyloo
6
6
  License: BSD-3-Clause
@@ -24,8 +24,9 @@ Classifier: Topic :: Internet
24
24
  Classifier: Topic :: Security
25
25
  Provides-Extra: docs
26
26
  Requires-Dist: Sphinx (<7.2) ; (python_version < "3.9") and (extra == "docs")
27
- Requires-Dist: Sphinx (>=7.2,<8.0) ; (python_version >= "3.9") and (extra == "docs")
28
- Requires-Dist: requests (>=2.31.0,<3.0.0)
27
+ Requires-Dist: Sphinx (>=7.2,<8.0) ; (python_version >= "3.9" and python_version < "3.10") and (extra == "docs")
28
+ Requires-Dist: Sphinx (>=8,<9) ; (python_version >= "3.10") and (extra == "docs")
29
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
29
30
  Project-URL: Documentation, https://pylookyloo.readthedocs.io/en/latest/
30
31
  Project-URL: Repository, https://github.com/lookyloo/PyLookyloo
31
32
  Description-Content-Type: text/markdown
@@ -2,12 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  import base64
6
7
  import warnings
7
8
 
9
+ from datetime import datetime
8
10
  from importlib.metadata import version
9
11
  from io import BytesIO, StringIO
10
- from typing import Any, TypedDict, overload
12
+ from typing import Any, TypedDict, overload, Literal
11
13
  from urllib.parse import urljoin, urlparse
12
14
  from pathlib import PurePosixPath, Path
13
15
 
@@ -40,6 +42,7 @@ class CaptureSettings(TypedDict, total=False):
40
42
  timezone_id: str | None
41
43
  locale: str | None
42
44
  color_scheme: str | None
45
+ java_script_enabled: bool
43
46
  viewport: dict[str, int] | None
44
47
  referer: str | None
45
48
 
@@ -145,6 +148,7 @@ class Lookyloo():
145
148
  timezone_id: str | None=None,
146
149
  locale: str | None=None,
147
150
  color_scheme: str | None=None,
151
+ java_script_enabled: bool=True,
148
152
  viewport: dict[str, int] | None=None,
149
153
  referer: str | None=None,
150
154
  listing: bool | None=None,
@@ -167,6 +171,7 @@ class Lookyloo():
167
171
  timezone_id: str | None=None,
168
172
  locale: str | None=None,
169
173
  color_scheme: str | None=None,
174
+ java_script_enabled: bool | None=None,
170
175
  viewport: dict[str, int] | None=None,
171
176
  referer: str | None=None,
172
177
  listing: bool | None=None,
@@ -193,6 +198,7 @@ class Lookyloo():
193
198
  :param timezone_id: The timezone, warning, it m ust be a valid timezone (continent/city)
194
199
  :param locale: The locale of the browser
195
200
  :param color_scheme: The prefered color scheme of the browser (light or dark)
201
+ :param java_script_enabled: If False, no JS will run during the capture.
196
202
  :param viewport: The viewport of the browser used for capturing
197
203
  :param referer: The referer URL for the capture
198
204
  :param listing: If False, the capture will be not be on the publicly accessible index page of lookyloo
@@ -244,6 +250,8 @@ class Lookyloo():
244
250
  to_send['locale'] = locale
245
251
  if color_scheme:
246
252
  to_send['color_scheme'] = color_scheme
253
+ if java_script_enabled is not None:
254
+ to_send['java_script_enabled'] = java_script_enabled
247
255
  if viewport:
248
256
  to_send['viewport'] = viewport
249
257
  if referer:
@@ -283,6 +291,14 @@ class Lookyloo():
283
291
  else:
284
292
  raise AuthError('Unable to initialize API key')
285
293
 
294
+ def get_user_config(self) -> dict[str, Any] | None:
295
+ '''Get the configuration enforced by the server for the current user (requires an authenticated user, use init_apikey first)
296
+ '''
297
+ if not self.apikey:
298
+ raise AuthError('You need to initialize the apikey to use this method (see init_apikey)')
299
+ r = self.session.get(urljoin(self.root_url, str(PurePosixPath('json', 'get_user_config'))))
300
+ return r.json()
301
+
286
302
  def misp_export(self, tree_uuid: str) -> dict[str, Any]:
287
303
  '''Export the capture in MISP format'''
288
304
  r = self.session.get(urljoin(self.root_url, str(PurePosixPath('json', tree_uuid, 'misp_export'))))
@@ -320,6 +336,13 @@ class Lookyloo():
320
336
  r = self.session.post(urljoin(self.root_url, str(PurePosixPath('admin', tree_uuid, 'hide'))))
321
337
  return r.json()
322
338
 
339
+ def remove_capture(self, tree_uuid: str) -> dict[str, str]:
340
+ '''Remove a capture, it will be impossible to get it by UUID (requires an authenticated user, use init_apikey first)'''
341
+ if not self.apikey:
342
+ raise AuthError('You need to initialize the apikey to use this method (see init_apikey)')
343
+ r = self.session.post(urljoin(self.root_url, str(PurePosixPath('admin', tree_uuid, 'remove'))))
344
+ return r.json()
345
+
323
346
  def get_redirects(self, capture_uuid: str) -> dict[str, Any]:
324
347
  '''Returns the initial redirects.
325
348
 
@@ -433,13 +456,22 @@ class Lookyloo():
433
456
  r = self.session.get(urljoin(self.root_url, str(PurePosixPath('json', 'stats'))))
434
457
  return r.json()
435
458
 
436
- def get_takedown_information(self, capture_uuid: str) -> dict[str, Any]:
459
+ @overload
460
+ def get_takedown_information(self, capture_uuid: str, filter_contacts: Literal[True]) -> list[str]:
461
+ ...
462
+
463
+ @overload
464
+ def get_takedown_information(self, capture_uuid: str, filter_contacts: Literal[False]=False) -> list[dict[str, Any]]:
465
+ ...
466
+
467
+ def get_takedown_information(self, capture_uuid: str, filter_contacts: bool=False) -> list[dict[str, Any]] | list[str]:
437
468
  '''Returns information required to request a takedown for a capture
438
469
 
439
470
  :param capture_uuid: UUID of the capture
471
+ :param filter_contacts: If True, will only return the contact emails and filter out the invalid ones.
440
472
  '''
441
473
  r = self.session.post(urljoin(self.root_url, str(PurePosixPath('json', 'takedown'))),
442
- json={'capture_uuid': capture_uuid})
474
+ json={'capture_uuid': capture_uuid, 'filter': filter_contacts})
443
475
  return r.json()
444
476
 
445
477
  def compare_captures(self, capture_left: str, capture_right: str, /, *, compare_settings: CompareSettings | None=None) -> dict[str, Any]:
@@ -464,8 +496,121 @@ class Lookyloo():
464
496
  return r.json()
465
497
 
466
498
  def send_mail(self, tree_uuid: str, email: str = '', comment: str | None = None) -> bool | dict[str, Any]:
499
+ '''Reports a capture by sending an email to the investigation team
500
+
501
+ :param tree_uuid: UUID of the capture
502
+ :param email: Email of the reporter, used by the analyst to get in touch
503
+ :param comment: Description of the URL, will be given to the analyst
504
+ '''
467
505
  to_send = {'email': email}
468
506
  if comment:
469
507
  to_send['comment'] = comment
470
508
  r = self.session.post(urljoin(self.root_url, str(PurePosixPath('json', tree_uuid, 'report'))), json=to_send)
471
509
  return r.json()
510
+
511
+ def get_recent_captures(self, timestamp: str | datetime | float | None=None) -> list[str]:
512
+ '''Gets the uuids of the most recent captures
513
+
514
+ :param timestamp: Oldest timestamp to consider
515
+ '''
516
+ if timestamp:
517
+ if isinstance(timestamp, datetime):
518
+ timestamp = timestamp.timestamp()
519
+ url = urljoin(self.root_url, str(PurePosixPath('json', 'recent_captures', str(timestamp))))
520
+ else:
521
+ url = urljoin(self.root_url, str(PurePosixPath('json', 'recent_captures')))
522
+ r = self.session.get(url)
523
+ return r.json()
524
+
525
+ def get_categories_captures(self, category: str | None=None) -> list[str] | dict[str, list[str]] | None:
526
+ '''Get uuids for a specific category or all categorized uuids if category is None
527
+
528
+ :param category: The category according to which the uuids are to be returned
529
+ '''
530
+ if category:
531
+ url = urljoin(self.root_url, str(PurePosixPath('json', 'categories', category)))
532
+ else:
533
+ url = urljoin(self.root_url, str(PurePosixPath('json', 'categories')))
534
+ r = self.session.get(url)
535
+ return r.json()
536
+
537
+ @overload
538
+ def upload_capture(self, *, quiet: Literal[True],
539
+ listing: bool = False,
540
+ full_capture: Path | BytesIO | str | None = None,
541
+ har: Path | BytesIO | str | None = None,
542
+ html: Path | BytesIO | str | None = None,
543
+ last_redirected_url: str | None = None,
544
+ screenshot: Path | BytesIO | str | None = None) -> str:
545
+ ...
546
+
547
+ @overload
548
+ def upload_capture(self, *, quiet: Literal[False]=False,
549
+ listing: bool = False,
550
+ full_capture: Path | BytesIO | str | None = None,
551
+ har: Path | BytesIO | str | None = None,
552
+ html: Path | BytesIO | str | None = None,
553
+ last_redirected_url: str | None = None,
554
+ screenshot: Path | BytesIO | str | None = None) -> tuple[str, dict[str, str]]:
555
+ ...
556
+
557
+ def upload_capture(self, *, quiet: bool = False,
558
+ listing: bool = False,
559
+ full_capture: Path | BytesIO | str | None = None,
560
+ har: Path | BytesIO | str | None = None,
561
+ html: Path | BytesIO | str | None = None,
562
+ last_redirected_url: str | None = None,
563
+ screenshot: Path | BytesIO | str | None = None) -> str | tuple[str, dict[str, str]]:
564
+ '''Upload a capture via har-file and others
565
+
566
+ :param quiet: Returns the UUID only, instead of the the UUID and the potential error / warning messages
567
+ :param listing: if true the capture should be public, else private - overwritten if the full_capture is given and it contains no_index
568
+ :param full_capture: path to the capture made by another instance
569
+ :param har: Harfile of the capture
570
+ :param html: rendered HTML of the capture
571
+ :param last_redirected_url: The landing page of the capture
572
+ :param screenshot: Screenshot of the capture
573
+ '''
574
+ def encode_document(document: Path | BytesIO | str) -> str:
575
+ if isinstance(document, str):
576
+ if not os.path.exists(document):
577
+ raise FileNotFoundError(f'{document} does not exist')
578
+ document = Path(document)
579
+ if isinstance(document, Path):
580
+ with document.open('rb') as f:
581
+ document = BytesIO(f.read())
582
+ return base64.b64encode(document.getvalue()).decode()
583
+
584
+ to_send: dict[str, Any] = {'listing': listing}
585
+
586
+ if full_capture:
587
+ b64_full_capture = encode_document(full_capture)
588
+ to_send['full_capture'] = b64_full_capture
589
+ elif har:
590
+ b64_har = encode_document(har)
591
+ to_send['har_file'] = b64_har
592
+
593
+ if html:
594
+ b64_html = encode_document(html)
595
+ to_send['html_file'] = b64_html
596
+
597
+ if last_redirected_url:
598
+ to_send['landing_page'] = last_redirected_url
599
+
600
+ if screenshot:
601
+ b64_screenshot = encode_document(screenshot)
602
+ to_send['screenshot_file'] = b64_screenshot
603
+ else:
604
+ raise PyLookylooError("Full capture or at least har-file are required")
605
+
606
+ r = self.session.post(urljoin(self.root_url, str(PurePosixPath('json', 'upload'))), json=to_send)
607
+ r.raise_for_status()
608
+ json_response = r.json()
609
+ uuid = json_response['uuid']
610
+ messages = json_response['messages']
611
+
612
+ if not uuid:
613
+ raise PyLookylooError('Unable to get UUID from lookyloo instance.')
614
+ if quiet:
615
+ return uuid
616
+ return uuid, messages
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pylookyloo"
3
- version = "1.24.0"
3
+ version = "1.26.1"
4
4
  description = "Python CLI and module for Lookyloo"
5
5
  authors = ["Raphaël Vinot <raphael.vinot@circl.lu>"]
6
6
  license = "BSD-3-Clause"
@@ -31,21 +31,22 @@ lookyloo = 'pylookyloo:main'
31
31
 
32
32
  [tool.poetry.dependencies]
33
33
  python = "^3.8"
34
- requests = "^2.31.0"
34
+ requests = "^2.32.3"
35
35
  Sphinx = [
36
36
  {version = "<7.2", python = "<3.9", optional = true},
37
- {version = "^7.2", python = ">=3.9", optional = true}
37
+ {version = "^7.2", python = ">=3.9,<3.10", optional = true},
38
+ {version = "^8", python = ">=3.10", optional = true}
38
39
  ]
39
40
 
40
41
  [tool.poetry.group.dev.dependencies]
41
- mypy = "^1.9.0"
42
- types-requests = "^2.31.0.20240311"
42
+ mypy = "^1.11.2"
43
+ types-requests = "^2.32.0.20240914"
43
44
  ipython = [
44
45
  {version = "<8.13.0", python = "<3.9"},
45
46
  {version = "^8.18.0", python = ">=3.9"},
46
47
  {version = "^8.19.0", python = ">=3.10"}
47
48
  ]
48
- pytest = "^8.1.1"
49
+ pytest = "^8.3.3"
49
50
 
50
51
  [tool.poetry.extras]
51
52
  docs = ["Sphinx"]
File without changes
File without changes