guarddog 2.0.0__py3-none-any.whl → 2.0.1__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.
@@ -10,6 +10,17 @@ rules:
10
10
  # (typically when a dependency is a git repository, see https://github.com/npm/cli/issues/6031#issuecomment-1449119423)
11
11
  # however this happens pretty rarely so reporting every package with a "prepare" script would be too noisy;
12
12
  # see https://github.com/DataDog/guarddog/issues/308
13
+ - pattern-not: |
14
+ "...": "npx only-allow pnpm"
15
+ - pattern-not: |
16
+ "...": ""
17
+ - pattern-not: |
18
+ "...": "patch-package"
19
+ - pattern-not: |
20
+ "...": "husky"
21
+ - pattern-not: |
22
+ "preinstall": "echo \"preinstall script\""
23
+
13
24
  - pattern-either:
14
25
  - pattern: |
15
26
  "preinstall": "..."
@@ -5,10 +5,9 @@ rules:
5
5
  metadata:
6
6
  description: Identify when a package contains an URL to a domain with a suspicious extension
7
7
  patterns:
8
- # Semgrep not robust enough to ignore comments in lists
9
- - pattern-not-regex: \# .*
10
8
 
11
9
  # ignore comments
10
+ - pattern-not-regex: ^\s*\# .*
12
11
  - pattern-not-regex: ^\s*\/\*(.|\n)*?\*\/\s*$
13
12
  - pattern-not-regex: ^\s*\/\/.*$
14
13
 
@@ -16,19 +15,22 @@ rules:
16
15
  - pattern-not-regex: ^\s*"""(.|\n)*?"""\s*$
17
16
 
18
17
  # Exclude local IPv4 sometimes used in tests
19
- - pattern-not-regex: (http[s]?:\/\/[^/?#]*(?:192\.168|10\.\d{1,3}|172\.(?:1[6-9]|2\d|3[0-1])|127\.\d{1,3})\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost)
18
+ - pattern-not-regex: (http[s]?:\/\/[^\n\[\/\?#"']*?(?:192\.168|10\.\d{1,3}|172\.(?:1[6-9]|2\d|3[0-1])|127\.\d{1,3})\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost)
20
19
 
21
20
  # Exclude public IPv4 sometimes used in tests
22
- - pattern-not-regex: (http[s]?:\/\/[^/?#]*(?:1\.1\.1\.1|8\.8\.8\.8))
21
+ - pattern-not-regex: (http[s]?:\/\/[^\n\[\/\?#"']*?(?:1\.1\.1\.1|8\.8\.8\.8))
23
22
 
24
23
  - patterns:
25
24
  - pattern: ("...")
26
25
  - pattern-either:
27
- - pattern-regex: (http[s]?:\/\/bit\.ly.*)$
28
- - pattern-regex: (http[s]?:\/\/.*\.(link|xyz|tk|ml|ga|cf|gq|pw|top|club|mw|bd|ke|am|sbs|date|quest|cd|bid|cd|ws|icu|cam|uno|email|stream))$
29
- - pattern-regex: (http[s]?:\/\/.*\.(link|xyz|tk|ml|ga|cf|gq|pw|top|club|mw|bd|ke|am|sbs|date|quest|cd|bid|cd|ws|icu|cam|uno|email|stream)\/)
30
- - pattern-regex: (http[s]?:\/\/[^/?#]*(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))
31
- - pattern-regex: (http[s]?:\/\/[^\n\[/?#]*?(?:\[(([A-Fa-f0-9]{1,4}:){0,7}|:):?[A-Fa-f0-9]{1,4}(:[A-Fa-f0-9]{1,4}){0,7})\])
26
+ # complete domains
27
+ - pattern-regex: (http[s]?:\/\/[^\n\[\/\?#"']*?(bit\.ly|discord\.com|workers\.dev|transfer\.sh|filetransfer\.io|sendspace\.com|appdomain\.cloud|backblazeb2\.com\|paste\.ee|ngrok\.io|termbin\.com|localhost\.run|webhook\.site|oastify\.com|burpcollaborator\.me)\/)
28
+ # top-level domains
29
+ - pattern-regex: (http[s]?:\/\/[^\n\[\/\?#"']*?\.(link|xyz|tk|ml|ga|cf|gq|pw|top|club|mw|bd|ke|am|sbs|date|quest|cd|bid|cd|ws|icu|cam|uno|email|stream)\/)
30
+ # IPv4
31
+ - pattern-regex: (http[s]?:\/\/[^\n\[\/\?#"']*?(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))
32
+ # IPv6
33
+ - pattern-regex: (http[s]?:\/\/[^\n\[\/\?#"']*?(?:\[(([A-Fa-f0-9]{1,4}:){0,7}|:):?[A-Fa-f0-9]{1,4}(:[A-Fa-f0-9]{1,4}){0,7})\])
32
34
  paths:
33
35
  exclude:
34
36
  - "*/test/*"
guarddog/cli.py CHANGED
@@ -9,7 +9,8 @@ import json as js
9
9
  import logging
10
10
  import os
11
11
  import sys
12
- from typing import Optional, cast
12
+ import tempfile
13
+ from typing import Optional
13
14
 
14
15
  import click
15
16
  from prettytable import PrettyTable
@@ -19,8 +20,8 @@ from guarddog.analyzer.metadata import get_metadata_detectors
19
20
  from guarddog.analyzer.sourcecode import get_sourcecode_rules
20
21
  from guarddog.ecosystems import ECOSYSTEM
21
22
  from guarddog.reporters.sarif import report_verify_sarif
22
- from guarddog.scanners import get_scanner
23
- from guarddog.scanners.scanner import PackageScanner
23
+ from guarddog.scanners import get_package_scanner, get_project_scanner
24
+ from guarddog.utils.archives import safe_extract
24
25
 
25
26
  EXIT_CODE_ISSUES_FOUND = 1
26
27
 
@@ -156,7 +157,7 @@ def _verify(
156
157
  """
157
158
  return_value = None
158
159
  rule_param = _get_rule_param(rules, exclude_rules, ecosystem)
159
- scanner = get_scanner(ecosystem, True)
160
+ scanner = get_project_scanner(ecosystem)
160
161
  if scanner is None:
161
162
  sys.stderr.write(f"Command verify is not supported for ecosystem {ecosystem}")
162
163
  exit(1)
@@ -208,46 +209,35 @@ def _scan(
208
209
  """
209
210
 
210
211
  rule_param = _get_rule_param(rules, exclude_rules, ecosystem)
211
- scanner = cast(Optional[PackageScanner], get_scanner(ecosystem, False))
212
+ scanner = get_package_scanner(ecosystem)
212
213
  if scanner is None:
213
214
  sys.stderr.write(f"Command scan is not supported for ecosystem {ecosystem}")
214
215
  sys.exit(1)
215
216
 
216
- results = []
217
- if os.path.isdir(identifier):
218
- log.debug(f"Considering that '{identifier}' is a local directory")
219
- for package in os.listdir(identifier):
220
- result = scanner.scan_local(f"{identifier}/{package}", rule_param)
221
- result["package"] = package
222
- results.append(result)
223
- elif os.path.isfile(identifier):
224
- log.debug(f"Considering that '{identifier}' is a local file")
225
- result = scanner.scan_local(identifier, rule_param)
226
- result["package"] = identifier
227
- results.append(result)
228
- else:
229
- log.debug(f"Considering that '{identifier}' is a remote target")
230
- try:
231
- result = scanner.scan_remote(identifier, version, rule_param)
232
- result["package"] = identifier
233
- results.append(result)
234
- except Exception as e:
235
- sys.stderr.write(f"\nError '{e}' occurred while scanning remote package.")
236
- sys.exit(1)
217
+ result = {"package": identifier}
218
+ try:
219
+ if os.path.isdir(identifier):
220
+ log.debug(f"Considering that '{identifier}' is a local directory")
221
+ result |= scanner.scan_local(identifier, rule_param)
222
+ elif os.path.isfile(identifier):
223
+ log.debug(f"Considering that '{identifier}' is a local archive file")
224
+ with tempfile.TemporaryDirectory() as tempdir:
225
+ safe_extract(identifier, tempdir)
226
+ result |= scanner.scan_local(tempdir, rule_param)
227
+ else:
228
+ log.debug(f"Considering that '{identifier}' is a remote target")
229
+ result |= scanner.scan_remote(identifier, version, rule_param)
230
+ except Exception as e:
231
+ sys.stderr.write(f"Error occurred while scanning target {identifier}: '{e}'\n")
232
+ sys.exit(1)
237
233
 
238
234
  if output_format == "json":
239
- if len(results) == 1:
240
- # return only a json like {}
241
- print(js.dumps(results[0]))
242
- else:
243
- # Return a list of result like [{},{}]
244
- print(js.dumps(results))
235
+ print(js.dumps(result))
245
236
  else:
246
- for result in results:
247
- print_scan_results(result, result["package"])
237
+ print_scan_results(result, result["package"])
248
238
 
249
239
  if exit_non_zero_on_finding:
250
- exit_with_status_code(results)
240
+ exit_with_status_code([result])
251
241
 
252
242
 
253
243
  def _list_rules(ecosystem: ECOSYSTEM):
@@ -6,22 +6,49 @@ from .pypi_package_scanner import PypiPackageScanner
6
6
  from .pypi_project_scanner import PypiRequirementsScanner
7
7
  from .go_package_scanner import GoModuleScanner
8
8
  from .go_project_scanner import GoDependenciesScanner
9
- from .scanner import Scanner
9
+ from .scanner import PackageScanner, ProjectScanner
10
10
  from ..ecosystems import ECOSYSTEM
11
11
 
12
12
 
13
- def get_scanner(ecosystem: ECOSYSTEM, project: bool) -> Optional[Scanner]:
14
- match (ecosystem, project):
15
- case (ECOSYSTEM.PYPI, False):
13
+ def get_package_scanner(ecosystem: ECOSYSTEM) -> Optional[PackageScanner]:
14
+ """
15
+ Return a `PackageScanner` for the given ecosystem or `None` if it
16
+ is not yet supported.
17
+
18
+ Args:
19
+ ecosystem (ECOSYSTEM): The ecosystem of the desired scanner
20
+
21
+ Returns:
22
+ Optional[PackageScanner]: The result of the scanner request
23
+
24
+ """
25
+ match ecosystem:
26
+ case ECOSYSTEM.PYPI:
16
27
  return PypiPackageScanner()
17
- case (ECOSYSTEM.PYPI, True):
18
- return PypiRequirementsScanner()
19
- case (ECOSYSTEM.NPM, False):
28
+ case ECOSYSTEM.NPM:
20
29
  return NPMPackageScanner()
21
- case (ECOSYSTEM.NPM, True):
22
- return NPMRequirementsScanner()
23
- case (ECOSYSTEM.GO, False):
30
+ case ECOSYSTEM.GO:
24
31
  return GoModuleScanner()
25
- case (ECOSYSTEM.GO, True):
32
+ return None
33
+
34
+
35
+ def get_project_scanner(ecosystem: ECOSYSTEM) -> Optional[ProjectScanner]:
36
+ """
37
+ Return a `ProjectScanner` for the given ecosystem or `None` if
38
+ it is not yet supported.
39
+
40
+ Args:
41
+ ecosystem (ECOSYSTEM): The ecosystem of the desired scanner
42
+
43
+ Returns:
44
+ Optional[ProjectScanner]: The result of the scanner request
45
+
46
+ """
47
+ match ecosystem:
48
+ case ECOSYSTEM.PYPI:
49
+ return PypiRequirementsScanner()
50
+ case ECOSYSTEM.NPM:
51
+ return NPMRequirementsScanner()
52
+ case ECOSYSTEM.GO:
26
53
  return GoDependenciesScanner()
27
54
  return None
@@ -21,18 +21,7 @@ def noop(arg: typing.Any) -> None:
21
21
  pass
22
22
 
23
23
 
24
- class Scanner:
25
- def __init__(self) -> None:
26
- pass
27
-
28
- @abstractmethod
29
- def scan_local(
30
- self, path, rules=None, callback: typing.Callable[[dict], None] = noop
31
- ):
32
- pass
33
-
34
-
35
- class ProjectScanner(Scanner):
24
+ class ProjectScanner:
36
25
  def __init__(self, package_scanner):
37
26
  super().__init__()
38
27
  self.package_scanner = package_scanner
@@ -212,7 +201,7 @@ class ProjectScanner(Scanner):
212
201
  pass
213
202
 
214
203
 
215
- class PackageScanner(Scanner):
204
+ class PackageScanner:
216
205
  """
217
206
  Scans package for attack vectors based on source code and metadata rules
218
207
 
@@ -231,7 +220,7 @@ class PackageScanner(Scanner):
231
220
  Scans local package
232
221
 
233
222
  Args:
234
- path (str): path to package
223
+ path (str): Path to the directory containing the package to analyze
235
224
  rules (set, optional): Set of rule names to use. Defaults to all rules.
236
225
  callback (typing.Callable[[dict], None], optional): Callback to apply to Analyzer output
237
226
 
@@ -245,16 +234,7 @@ class PackageScanner(Scanner):
245
234
  if rules is not None:
246
235
  rules = set(rules)
247
236
 
248
- results = None
249
- if os.path.isdir(path):
250
- results = self.analyzer.analyze_sourcecode(path, rules=rules)
251
- elif os.path.isfile(path):
252
- with tempfile.TemporaryDirectory() as tempdir:
253
- safe_extract(path, tempdir)
254
- results = self.analyzer.analyze_sourcecode(tempdir, rules=rules)
255
- else:
256
- raise Exception(f"Local scan target {path} is neither a directory nor a file.")
257
-
237
+ results = self.analyzer.analyze_sourcecode(path, rules=rules)
258
238
  callback(results)
259
239
 
260
240
  return results
@@ -1,5 +1,6 @@
1
1
  import logging
2
2
  import os
3
+ import stat
3
4
  import zipfile
4
5
 
5
6
  import tarsafe # type:ignore
@@ -9,41 +10,25 @@ log = logging.getLogger("guarddog")
9
10
 
10
11
  def is_supported_archive(path: str) -> bool:
11
12
  """
12
- Decide whether a file contains a supported archive.
13
+ Decide whether a file contains a supported archive based on its
14
+ file extension.
13
15
 
14
16
  Args:
15
17
  path (str): The local filesystem path to examine
16
18
 
17
19
  Returns:
18
20
  bool: Represents the decision reached for the file
19
- """
20
- return is_tar_archive(path) or is_zip_archive(path)
21
-
22
-
23
- def is_tar_archive(path: str) -> bool:
24
- """
25
- Decide whether a file contains a tar archive.
26
-
27
- Args:
28
- path (str): The local filesystem path to examine
29
21
 
30
- Returns:
31
- bool: Represents the decision reached for the file
32
22
  """
33
- return any(path.endswith(ext) for ext in [".tar.gz", ".tgz"])
23
+ def is_tar_archive(path: str) -> bool:
24
+ tar_exts = [".bz2", ".bzip2", ".gz", ".gzip", ".tgz", ".xz"]
34
25
 
26
+ return any(path.endswith(ext) for ext in tar_exts)
35
27
 
36
- def is_zip_archive(path: str) -> bool:
37
- """
38
- Decide whether a file contains a zip, whl or egg archive.
28
+ def is_zip_archive(path: str) -> bool:
29
+ return any(path.endswith(ext) for ext in [".zip", ".whl", ".egg"])
39
30
 
40
- Args:
41
- path (str): The local filesystem path to examine
42
-
43
- Returns:
44
- bool: Represents the decision reached for the file
45
- """
46
- return any(path.endswith(ext) for ext in [".zip", ".whl", ".egg"])
31
+ return is_tar_archive(path) or is_zip_archive(path)
47
32
 
48
33
 
49
34
  def safe_extract(source_archive: str, target_directory: str) -> None:
@@ -61,9 +46,28 @@ def safe_extract(source_archive: str, target_directory: str) -> None:
61
46
 
62
47
  """
63
48
  log.debug(f"Extracting archive {source_archive} to directory {target_directory}")
64
- if is_tar_archive(source_archive):
49
+ if tarsafe.is_tarfile(source_archive):
50
+
51
+ def add_exec(path):
52
+ st = os.stat(path)
53
+ os.chmod(path, st.st_mode | stat.S_IEXEC)
54
+
55
+ def add_read(path):
56
+ st = os.stat(path)
57
+ os.chmod(path, st.st_mode | stat.S_IREAD)
58
+
59
+ def recurse_add_perms(path):
60
+ add_exec(path)
61
+ for root, dirs, files in os.walk(path):
62
+ for d in dirs:
63
+ add_exec(os.path.join(root, d))
64
+ for f in files:
65
+ add_read(os.path.join(root, f))
66
+
65
67
  tarsafe.open(source_archive).extractall(target_directory)
66
- elif is_zip_archive(source_archive):
68
+ recurse_add_perms(target_directory)
69
+
70
+ elif zipfile.is_zipfile(source_archive):
67
71
  with zipfile.ZipFile(source_archive, 'r') as zip:
68
72
  for file in zip.namelist():
69
73
  # Note: zip.extract cleans up any malicious file name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: guarddog
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: GuardDog is a CLI tool to Identify malicious PyPI packages
5
5
  Home-page: https://github.com/DataDog/guarddog
6
6
  License: Apache-2.0
@@ -51,36 +51,36 @@ guarddog/analyzer/sourcecode/exfiltrate-sensitive-data.yml,sha256=e8dj10HABb36rS
51
51
  guarddog/analyzer/sourcecode/npm-dll-hijacking.yml,sha256=TPIXvWm8Ot9RVtDXWFmoNZw9-3PH7NuXK6x1fQCiRt4,3506
52
52
  guarddog/analyzer/sourcecode/npm-exec-base64.yml,sha256=xNIwJAmGP19wvxH_w1ySgDsxrUU3GkrxRcFjjnB9fWM,576
53
53
  guarddog/analyzer/sourcecode/npm-exfiltrate-sensitive-data.yml,sha256=UP-GlZ5VykHWFebgIiHrkrQL9PdtjxR99_m2FZddmuw,3011
54
- guarddog/analyzer/sourcecode/npm-install-script.yml,sha256=5wKz372bvqBdIzXjCUeZthg9iVnbK2ZImTZKwqcWwQc,1067
54
+ guarddog/analyzer/sourcecode/npm-install-script.yml,sha256=0resBD7upjukUWsUEYv9sWLC1bCN8xD1pgCVDAxYa_I,1355
55
55
  guarddog/analyzer/sourcecode/npm-obfuscation.yml,sha256=BIyf7PP8F2dTX4BIWDiBOqDg0Wlj6_PRxe5pWMXUqeQ,1815
56
56
  guarddog/analyzer/sourcecode/npm-serialize-environment.yml,sha256=gFpr58INp44ZwxYZlIHyzpOgbVMDLv1ZRPTGAczX5dw,835
57
57
  guarddog/analyzer/sourcecode/npm-silent-process-execution.yml,sha256=qnJHGesNPNpxGa8n2kQMpttLGck-6vZjI_SsweDyk7M,3513
58
58
  guarddog/analyzer/sourcecode/npm-steganography.yml,sha256=XH0udcriAQq_6WOHAG4TpIedw8GgKyWx9gsG_Q_Fki8,915
59
59
  guarddog/analyzer/sourcecode/obfuscation.yml,sha256=QgvqJJ8ovyZ4NSjNGJabQiRJTKIExUCevnhLksueh9M,582
60
- guarddog/analyzer/sourcecode/shady-links.yml,sha256=umHNN1y5G8Tm-vc5_L_s3fTG67yt9kVZWZTlDnTi6Kw,1806
60
+ guarddog/analyzer/sourcecode/shady-links.yml,sha256=a0TDoyVXes13VjSeg6NECtNVJWNigqQU54aELiJZ5qQ,1961
61
61
  guarddog/analyzer/sourcecode/silent-process-execution.yml,sha256=b6RjenMv7si7lXGak3uMmD7PMtQRuKPeJFggPW6UDNI,418
62
62
  guarddog/analyzer/sourcecode/steganography.yml,sha256=3ceO6SJhu4XpZEjfwelLdOxeZ4Ho1OgUjbcacwtOhR0,606
63
- guarddog/cli.py,sha256=g9CL8_vQnnNC_H-m-stdP0T1_-QannVp7CTZ-IgverU,13537
63
+ guarddog/cli.py,sha256=P5pc_qkX_SHPHRoPnjjq7au2Vj7GYW906r7dh6ADzg4,13201
64
64
  guarddog/ecosystems.py,sha256=kgM4v5E8PZBQksWgzuWwODS5R7P16klDi1SGWKLy1e0,380
65
65
  guarddog/reporters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
66
  guarddog/reporters/sarif.py,sha256=44Ixtfk9IhHHaU0MtRLsaBUnPl23kDRuCbtWJ84Z9LA,5866
67
- guarddog/scanners/__init__.py,sha256=BT4KfdXmJzY6SG0GUVuHJTcBgTP37Yot1PddV3katgw,1005
67
+ guarddog/scanners/__init__.py,sha256=yEsDvThkIAhFkP59gSCFxYe5HTLmoSzfjAkDrtFT1LY,1628
68
68
  guarddog/scanners/go_package_scanner.py,sha256=OdCbwtjJow9AxEv34z7WBfgTamqKj5DxJh7dly_1NuY,2926
69
69
  guarddog/scanners/go_project_scanner.py,sha256=3D5dYSA7FVqc7IIM7uAHlCJZalshP_WhagWmOcYirog,2123
70
70
  guarddog/scanners/npm_package_scanner.py,sha256=qBU0tCbW2pTL3cy5Y4JVAJyAGdvb-HY69qSQmjWbPxU,1968
71
71
  guarddog/scanners/npm_project_scanner.py,sha256=L_gqinZit6KHE0dJTRnuJ49U4E3izNf4UBVGkHkiPjw,3585
72
72
  guarddog/scanners/pypi_package_scanner.py,sha256=Tg7M837vhNZim3Jy9OMJSQY2C_m9C75UDy0S_5WKT6M,2375
73
73
  guarddog/scanners/pypi_project_scanner.py,sha256=NY-xO27r9xIGik7y-btoBKX54_VPSV_RJfeClJDKAkA,5049
74
- guarddog/scanners/scanner.py,sha256=Jx1EU267GT5cHvQRvFT3ua4Zq459eFxVDtLGKSRO5aU,11494
74
+ guarddog/scanners/scanner.py,sha256=7-OGs8GoRfyexEYOfVRSmV7P-7ZJDXtgj2Z1UrKGx30,10929
75
75
  guarddog/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
76
- guarddog/utils/archives.py,sha256=jmMoLOew5idgSnvvTVKKPdb8paT9uCEVm7wfBxHuKjI,2335
76
+ guarddog/utils/archives.py,sha256=jOXAhxZx-mTtpDidGGKxQg052CvaQOAVklvOeUn9HTQ,2593
77
77
  guarddog/utils/config.py,sha256=KCiGsaeautOo-1p0CSbHY5HWT8cC17-_8tqKH7hPa1E,883
78
78
  guarddog/utils/exceptions.py,sha256=23Kzl3exqYK6X-bcGUeb8wPmSglWNX3GIDPkJ6lQzo4,54
79
79
  guarddog/utils/package_info.py,sha256=TFjE1xsGNf60SuHlIeDV2pzMUbogl5TKJdSzswat6jI,953
80
- guarddog-2.0.0.dist-info/LICENSE,sha256=w1aNZxHyoyOPJ4fSdiyrr06tCJZbTjCsH9K1uqeDVyU,11377
81
- guarddog-2.0.0.dist-info/LICENSE-3rdparty.csv,sha256=cS61ONZL_xlXaTMvQXyBEi3J3es-40Gg6G-6idoa5Qk,314
82
- guarddog-2.0.0.dist-info/METADATA,sha256=opZAQ_WucUlT9-_nQW2rm3acUeqX8HBGWUUY1hDke5A,1417
83
- guarddog-2.0.0.dist-info/NOTICE,sha256=nlyNt2IjG8IBoQkb7n6jszwAvmREpKAx0POzFO1s2JM,140
84
- guarddog-2.0.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
85
- guarddog-2.0.0.dist-info/entry_points.txt,sha256=vX2fvhnNdkbEL4pDzrH2NqjWVxeOaEYi0sJYmNgS2-s,45
86
- guarddog-2.0.0.dist-info/RECORD,,
80
+ guarddog-2.0.1.dist-info/LICENSE,sha256=w1aNZxHyoyOPJ4fSdiyrr06tCJZbTjCsH9K1uqeDVyU,11377
81
+ guarddog-2.0.1.dist-info/LICENSE-3rdparty.csv,sha256=cS61ONZL_xlXaTMvQXyBEi3J3es-40Gg6G-6idoa5Qk,314
82
+ guarddog-2.0.1.dist-info/METADATA,sha256=HRjSkYeNAJ7XuQA5C_namTiG6vt0-qpaE-6nNGMhX88,1417
83
+ guarddog-2.0.1.dist-info/NOTICE,sha256=nlyNt2IjG8IBoQkb7n6jszwAvmREpKAx0POzFO1s2JM,140
84
+ guarddog-2.0.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
85
+ guarddog-2.0.1.dist-info/entry_points.txt,sha256=vX2fvhnNdkbEL4pDzrH2NqjWVxeOaEYi0sJYmNgS2-s,45
86
+ guarddog-2.0.1.dist-info/RECORD,,