guarddog 2.5.0__py3-none-any.whl → 2.7.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.
- guarddog/analyzer/analyzer.py +58 -20
- guarddog/analyzer/metadata/__init__.py +2 -0
- guarddog/analyzer/metadata/bundled_binary.py +6 -6
- guarddog/analyzer/metadata/deceptive_author.py +3 -1
- guarddog/analyzer/metadata/detector.py +7 -2
- guarddog/analyzer/metadata/empty_information.py +8 -3
- guarddog/analyzer/metadata/go/typosquatting.py +4 -3
- guarddog/analyzer/metadata/npm/bundled_binary.py +7 -2
- guarddog/analyzer/metadata/npm/deceptive_author.py +1 -1
- guarddog/analyzer/metadata/npm/direct_url_dependency.py +2 -1
- guarddog/analyzer/metadata/npm/empty_information.py +10 -7
- guarddog/analyzer/metadata/npm/potentially_compromised_email_domain.py +4 -3
- guarddog/analyzer/metadata/npm/release_zero.py +13 -5
- guarddog/analyzer/metadata/npm/typosquatting.py +1 -1
- guarddog/analyzer/metadata/npm/unclaimed_maintainer_email_domain.py +3 -2
- guarddog/analyzer/metadata/npm/utils.py +4 -5
- guarddog/analyzer/metadata/potentially_compromised_email_domain.py +8 -4
- guarddog/analyzer/metadata/pypi/__init__.py +12 -6
- guarddog/analyzer/metadata/pypi/bundled_binary.py +7 -2
- guarddog/analyzer/metadata/pypi/deceptive_author.py +1 -1
- guarddog/analyzer/metadata/pypi/empty_information.py +16 -5
- guarddog/analyzer/metadata/pypi/potentially_compromised_email_domain.py +4 -3
- guarddog/analyzer/metadata/pypi/release_zero.py +16 -6
- guarddog/analyzer/metadata/pypi/repository_integrity_mismatch.py +53 -27
- guarddog/analyzer/metadata/pypi/single_python_file.py +9 -4
- guarddog/analyzer/metadata/pypi/typosquatting.py +21 -8
- guarddog/analyzer/metadata/pypi/unclaimed_maintainer_email_domain.py +6 -2
- guarddog/analyzer/metadata/pypi/utils.py +1 -4
- guarddog/analyzer/metadata/release_zero.py +1 -1
- guarddog/analyzer/metadata/repository_integrity_mismatch.py +10 -3
- guarddog/analyzer/metadata/resources/top_pypi_packages.json +43984 -15984
- guarddog/analyzer/metadata/typosquatting.py +12 -8
- guarddog/analyzer/metadata/unclaimed_maintainer_email_domain.py +7 -2
- guarddog/analyzer/sourcecode/__init__.py +34 -7
- guarddog/analyzer/sourcecode/api-obfuscation.yml +42 -0
- guarddog/analyzer/sourcecode/code-execution.yml +1 -0
- guarddog/analyzer/sourcecode/dll-hijacking.yml +5 -0
- guarddog/analyzer/sourcecode/go-exec-base64.yml +40 -0
- guarddog/analyzer/sourcecode/go-exec-download.yml +85 -0
- guarddog/analyzer/sourcecode/go-exfiltrate-sensitive-data.yml +85 -0
- guarddog/analyzer/sourcecode/npm-obfuscation.yml +2 -1
- guarddog/analyzer/sourcecode/shady-links.yml +2 -0
- guarddog/analyzer/sourcecode/suspicious_passwd_access_linux.yar +12 -0
- guarddog/analyzer/sourcecode/unicode.yml +75 -0
- guarddog/cli.py +33 -107
- guarddog/ecosystems.py +3 -0
- guarddog/reporters/__init__.py +28 -0
- guarddog/reporters/human_readable.py +138 -0
- guarddog/reporters/json.py +28 -0
- guarddog/reporters/reporter_factory.py +50 -0
- guarddog/reporters/sarif.py +179 -173
- guarddog/scanners/__init__.py +5 -0
- guarddog/scanners/extension_scanner.py +152 -0
- guarddog/scanners/github_action_project_scanner.py +47 -8
- guarddog/scanners/github_action_scanner.py +6 -2
- guarddog/scanners/go_project_scanner.py +42 -5
- guarddog/scanners/npm_package_scanner.py +12 -4
- guarddog/scanners/npm_project_scanner.py +54 -10
- guarddog/scanners/pypi_package_scanner.py +9 -3
- guarddog/scanners/pypi_project_scanner.py +67 -29
- guarddog/scanners/scanner.py +247 -164
- guarddog/utils/archives.py +2 -1
- guarddog/utils/package_info.py +3 -1
- {guarddog-2.5.0.dist-info → guarddog-2.7.0.dist-info}/METADATA +11 -10
- guarddog-2.7.0.dist-info/RECORD +100 -0
- {guarddog-2.5.0.dist-info → guarddog-2.7.0.dist-info}/WHEEL +1 -1
- guarddog-2.5.0.dist-info/RECORD +0 -90
- {guarddog-2.5.0.dist-info → guarddog-2.7.0.dist-info}/entry_points.txt +0 -0
- {guarddog-2.5.0.dist-info → guarddog-2.7.0.dist-info/licenses}/LICENSE +0 -0
- {guarddog-2.5.0.dist-info → guarddog-2.7.0.dist-info/licenses}/LICENSE-3rdparty.csv +0 -0
- {guarddog-2.5.0.dist-info → guarddog-2.7.0.dist-info/licenses}/NOTICE +0 -0
guarddog/scanners/scanner.py
CHANGED
|
@@ -2,11 +2,12 @@ import concurrent.futures
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
import sys
|
|
6
5
|
import tempfile
|
|
7
6
|
import typing
|
|
8
7
|
from abc import abstractmethod
|
|
9
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import List, Optional, Set, Tuple
|
|
10
11
|
|
|
11
12
|
import requests
|
|
12
13
|
|
|
@@ -21,183 +22,66 @@ def noop(arg: typing.Any) -> None:
|
|
|
21
22
|
pass
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _authenticate_by_access_token(self) -> tuple[str, str]:
|
|
30
|
-
"""
|
|
31
|
-
Gives GitHub authentication through access token
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
tuple[str, str]: username, personal access token
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
user = os.getenv("GIT_USERNAME")
|
|
38
|
-
personal_access_token = os.getenv("GH_TOKEN")
|
|
39
|
-
if not user or not personal_access_token:
|
|
40
|
-
log.error(
|
|
41
|
-
"""WARNING: Please set GIT_USERNAME (Github handle) and GH_TOKEN
|
|
42
|
-
(generate a personal access token in Github settings > developer)
|
|
43
|
-
as environment variables before proceeding."""
|
|
44
|
-
)
|
|
45
|
-
exit(1)
|
|
46
|
-
return (user, personal_access_token)
|
|
47
|
-
|
|
48
|
-
def scan_requirements(
|
|
49
|
-
self,
|
|
50
|
-
requirements: str,
|
|
51
|
-
rules=None,
|
|
52
|
-
callback: typing.Callable[[dict], None] = noop,
|
|
53
|
-
) -> dict:
|
|
54
|
-
"""
|
|
55
|
-
Reads the requirements.txt file and scans each possible
|
|
56
|
-
dependency and version
|
|
57
|
-
|
|
58
|
-
Args:
|
|
59
|
-
requirements (str): contents of requirements.txt file
|
|
60
|
-
rules: list of rules to apply
|
|
61
|
-
callback: callback to call for each result
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
dict: mapping of dependencies to scan results
|
|
65
|
-
|
|
66
|
-
ex.
|
|
67
|
-
{
|
|
68
|
-
....
|
|
69
|
-
<dependency-name>: {
|
|
70
|
-
issues: ...,
|
|
71
|
-
results: {
|
|
72
|
-
...
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
...
|
|
76
|
-
}
|
|
77
|
-
"""
|
|
78
|
-
|
|
79
|
-
def scan_single_dependency(dependency, version):
|
|
80
|
-
log.debug(f"Scanning {dependency} version {version}")
|
|
81
|
-
result = self.package_scanner.scan_remote(dependency, version, rules)
|
|
82
|
-
return {"dependency": dependency, "version": version, "result": result}
|
|
83
|
-
|
|
84
|
-
dependencies = self.parse_requirements(requirements)
|
|
85
|
-
num_workers = PARALLELISM
|
|
86
|
-
|
|
87
|
-
log.info(
|
|
88
|
-
f"Scanning using at most {num_workers} parallel worker threads\n"
|
|
89
|
-
)
|
|
90
|
-
with ThreadPoolExecutor(max_workers=num_workers) as pool:
|
|
91
|
-
try:
|
|
92
|
-
futures: typing.List[concurrent.futures.Future] = []
|
|
93
|
-
for dependency, versions in dependencies.items():
|
|
94
|
-
assert versions is None or len(versions) > 0
|
|
95
|
-
if versions is None:
|
|
96
|
-
# this will cause scan_remote to use the latest version
|
|
97
|
-
futures.append(
|
|
98
|
-
pool.submit(scan_single_dependency, dependency, None)
|
|
99
|
-
)
|
|
100
|
-
else:
|
|
101
|
-
futures.extend(
|
|
102
|
-
map(
|
|
103
|
-
lambda version: pool.submit(
|
|
104
|
-
scan_single_dependency, dependency, version
|
|
105
|
-
),
|
|
106
|
-
versions,
|
|
107
|
-
)
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
results = []
|
|
111
|
-
for future in concurrent.futures.as_completed(futures):
|
|
112
|
-
result = future.result()
|
|
113
|
-
if callback is not None:
|
|
114
|
-
callback(result)
|
|
115
|
-
results.append(result)
|
|
116
|
-
except KeyboardInterrupt:
|
|
117
|
-
log.warning("Received keyboard interrupt, cancelling scan\n")
|
|
118
|
-
pool.shutdown(wait=False, cancel_futures=True)
|
|
25
|
+
@dataclass
|
|
26
|
+
class DependencyVersion:
|
|
27
|
+
"""
|
|
28
|
+
This class represents the identified dependency versions in a project,
|
|
29
|
+
usually defined in a specification file (requirements.txt, package.json, etc.)
|
|
119
30
|
|
|
120
|
-
|
|
31
|
+
Attributes:
|
|
32
|
+
version (str): The version of the dependency. e.g., "1.0.0"
|
|
33
|
+
location (int): This indicates the line number in the specification file where the dependency is defined.
|
|
34
|
+
"""
|
|
121
35
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
Scans remote requirements.txt file
|
|
36
|
+
version: str # the version number of the dependency
|
|
37
|
+
location: int
|
|
125
38
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
39
|
+
def __eq__(self, other):
|
|
40
|
+
if isinstance(other, str):
|
|
41
|
+
return self.version == other
|
|
42
|
+
if isinstance(other, DependencyVersion):
|
|
43
|
+
return self.version == other.version
|
|
44
|
+
return NotImplemented
|
|
131
45
|
|
|
132
|
-
|
|
133
|
-
|
|
46
|
+
def __hash__(self):
|
|
47
|
+
return hash(self.version)
|
|
134
48
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
....
|
|
138
|
-
<dependency-name>: {
|
|
139
|
-
issues: ...,
|
|
140
|
-
results: {
|
|
141
|
-
...
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
...
|
|
145
|
-
}
|
|
146
|
-
"""
|
|
49
|
+
def __repr__(self):
|
|
50
|
+
return f"DependencyVersion({self.version!r})"
|
|
147
51
|
|
|
148
|
-
token = self._authenticate_by_access_token()
|
|
149
|
-
githubusercontent_url = url.replace("github", "raw.githubusercontent")
|
|
150
52
|
|
|
151
|
-
|
|
152
|
-
|
|
53
|
+
@dataclass
|
|
54
|
+
class Dependency:
|
|
55
|
+
"""
|
|
56
|
+
This class represents a dependency in a project, usually defined in a specification file
|
|
153
57
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
f"{req_url} does not exist. Check your link or branch name."
|
|
159
|
-
)
|
|
160
|
-
sys.exit(255)
|
|
58
|
+
Attributes:
|
|
59
|
+
name (str): The name of the dependency. e.g., "requests"
|
|
60
|
+
versions (Set[DependencyVersion]): A set of identified versions of the dependency.
|
|
61
|
+
"""
|
|
161
62
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
):
|
|
165
|
-
"""
|
|
166
|
-
Scans a local requirements.txt file
|
|
63
|
+
name: str
|
|
64
|
+
versions: Set[DependencyVersion]
|
|
167
65
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
66
|
+
def __eq__(self, other):
|
|
67
|
+
if isinstance(other, str):
|
|
68
|
+
return self.name == other
|
|
69
|
+
if isinstance(other, Dependency):
|
|
70
|
+
return self.name == other.name
|
|
71
|
+
return NotImplemented
|
|
172
72
|
|
|
173
|
-
|
|
174
|
-
|
|
73
|
+
def __repr__(self):
|
|
74
|
+
return f"Dependency({self.name!r})"
|
|
175
75
|
|
|
176
|
-
ex.
|
|
177
|
-
{
|
|
178
|
-
....
|
|
179
|
-
<dependency-name>: {
|
|
180
|
-
issues: ...,
|
|
181
|
-
results: {
|
|
182
|
-
...
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
...
|
|
186
|
-
}
|
|
187
|
-
"""
|
|
188
76
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
sys.exit(255)
|
|
77
|
+
@dataclass
|
|
78
|
+
class DependencyFile:
|
|
79
|
+
"""
|
|
80
|
+
This class represents a specification file for a project (requirements.txt, package.json, etc.)
|
|
81
|
+
"""
|
|
195
82
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
self, raw_requirements: str
|
|
199
|
-
) -> dict[str, set[str]]: # returns { package: version }
|
|
200
|
-
pass
|
|
83
|
+
file_path: str
|
|
84
|
+
dependencies: List[Dependency]
|
|
201
85
|
|
|
202
86
|
|
|
203
87
|
class PackageScanner:
|
|
@@ -324,3 +208,202 @@ class PackageScanner:
|
|
|
324
208
|
finally:
|
|
325
209
|
log.debug(f"Removing temporary archive file {archive_path}")
|
|
326
210
|
os.remove(archive_path)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ProjectScanner:
|
|
214
|
+
def __init__(self, package_scanner: PackageScanner):
|
|
215
|
+
super().__init__()
|
|
216
|
+
self.package_scanner = package_scanner
|
|
217
|
+
|
|
218
|
+
def _authenticate_by_access_token(self) -> tuple[str, str]:
|
|
219
|
+
"""
|
|
220
|
+
Gives GitHub authentication through access token
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
tuple[str, str]: username, personal access token
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
user = os.getenv("GIT_USERNAME")
|
|
227
|
+
personal_access_token = os.getenv("GH_TOKEN")
|
|
228
|
+
if not user or not personal_access_token:
|
|
229
|
+
log.error(
|
|
230
|
+
"""WARNING: Please set GIT_USERNAME (Github handle) and GH_TOKEN
|
|
231
|
+
(generate a personal access token in Github settings > developer)
|
|
232
|
+
as environment variables before proceeding."""
|
|
233
|
+
)
|
|
234
|
+
exit(1)
|
|
235
|
+
return (user, personal_access_token)
|
|
236
|
+
|
|
237
|
+
def scan_dependencies(
|
|
238
|
+
self,
|
|
239
|
+
dependencies: List[Dependency],
|
|
240
|
+
rules=None,
|
|
241
|
+
callback: typing.Callable[[dict], None] = noop,
|
|
242
|
+
) -> list[dict]:
|
|
243
|
+
"""
|
|
244
|
+
scans each possible dependency and version supplied
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
dependencies a list of dependencies to scan
|
|
248
|
+
rules: list of rules to apply
|
|
249
|
+
callback: callback to call for each result
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
dict: mapping of dependencies to scan results
|
|
253
|
+
|
|
254
|
+
ex.
|
|
255
|
+
{
|
|
256
|
+
....
|
|
257
|
+
<dependency-name>: {
|
|
258
|
+
issues: ...,
|
|
259
|
+
results: {
|
|
260
|
+
...
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
...
|
|
264
|
+
}
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
def scan_single_dependency(dependency: str, version: Optional[str]) -> dict:
|
|
268
|
+
log.debug(f"Scanning {dependency} version {version}")
|
|
269
|
+
result = self.package_scanner.scan_remote(dependency, version, rules)
|
|
270
|
+
return {"dependency": dependency, "version": version, "result": result}
|
|
271
|
+
|
|
272
|
+
num_workers = PARALLELISM
|
|
273
|
+
|
|
274
|
+
log.info(f"Scanning using at most {num_workers} parallel worker threads\n")
|
|
275
|
+
with ThreadPoolExecutor(max_workers=num_workers) as pool:
|
|
276
|
+
try:
|
|
277
|
+
futures: typing.List[concurrent.futures.Future] = []
|
|
278
|
+
for dependency in dependencies:
|
|
279
|
+
versions = dependency.versions
|
|
280
|
+
if not versions:
|
|
281
|
+
# this will cause scan_remote to use the latest version
|
|
282
|
+
futures.append(
|
|
283
|
+
pool.submit(scan_single_dependency, dependency.name, None)
|
|
284
|
+
)
|
|
285
|
+
else:
|
|
286
|
+
futures.extend(
|
|
287
|
+
map(
|
|
288
|
+
lambda version: pool.submit(
|
|
289
|
+
scan_single_dependency,
|
|
290
|
+
dependency.name,
|
|
291
|
+
version.version,
|
|
292
|
+
),
|
|
293
|
+
versions,
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
results = []
|
|
298
|
+
for future in concurrent.futures.as_completed(futures):
|
|
299
|
+
result = future.result()
|
|
300
|
+
if callback is not None:
|
|
301
|
+
callback(result)
|
|
302
|
+
results.append(result)
|
|
303
|
+
except KeyboardInterrupt:
|
|
304
|
+
log.warning("Received keyboard interrupt, cancelling scan\n")
|
|
305
|
+
pool.shutdown(wait=False, cancel_futures=True)
|
|
306
|
+
|
|
307
|
+
return results
|
|
308
|
+
|
|
309
|
+
def scan_remote(
|
|
310
|
+
self, url: str, branch: str, requirements_name: str
|
|
311
|
+
) -> tuple[List[Dependency], list[dict]]:
|
|
312
|
+
"""
|
|
313
|
+
Scans remote requirements.txt file
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
url (str): url of the GitHub repo
|
|
317
|
+
branch (str): branch containing requirements.txt
|
|
318
|
+
requirements_name (str, optional): name of requirements file.
|
|
319
|
+
Defaults to "requirements.txt".
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
deps: list of dependencies to scan
|
|
323
|
+
results: mapping of dependencies to scan results
|
|
324
|
+
ex.
|
|
325
|
+
{
|
|
326
|
+
....
|
|
327
|
+
<dependency-name>: {
|
|
328
|
+
issues: ...,
|
|
329
|
+
results: {
|
|
330
|
+
...
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
...
|
|
334
|
+
}
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
token = self._authenticate_by_access_token()
|
|
338
|
+
githubusercontent_url = url.replace("github", "raw.githubusercontent")
|
|
339
|
+
req_url = f"{githubusercontent_url}/{branch}/{requirements_name}"
|
|
340
|
+
resp = requests.get(url=req_url, auth=token)
|
|
341
|
+
resp.raise_for_status()
|
|
342
|
+
dependencies = self.parse_requirements(resp.content.decode())
|
|
343
|
+
return dependencies, self.scan_dependencies(dependencies)
|
|
344
|
+
|
|
345
|
+
def scan_local(
|
|
346
|
+
self, path, rules=None, callback: typing.Callable[[dict], None] = noop
|
|
347
|
+
) -> Tuple[List[DependencyFile], list[dict]]:
|
|
348
|
+
"""
|
|
349
|
+
Scans a local requirements files (requirements.txt, package.json, etc.)
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
path (str): path to requirements file or directory to search
|
|
353
|
+
rules: list of rules to apply
|
|
354
|
+
callback: callback to call for each result
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
deps: list of dependencies to scan
|
|
358
|
+
results: mapping of dependencies to scan results
|
|
359
|
+
ex.
|
|
360
|
+
{
|
|
361
|
+
....
|
|
362
|
+
<dependency-name>: {
|
|
363
|
+
issues: ...,
|
|
364
|
+
results: {
|
|
365
|
+
...
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
...
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
requirement_paths = []
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
if os.path.isfile(path):
|
|
377
|
+
requirement_paths.append(path)
|
|
378
|
+
elif os.path.isdir(path):
|
|
379
|
+
requirement_paths.extend(self.find_requirements(path))
|
|
380
|
+
else:
|
|
381
|
+
raise ValueError(f"unable to find file or directory {path}")
|
|
382
|
+
|
|
383
|
+
dep_files: List[DependencyFile] = []
|
|
384
|
+
|
|
385
|
+
for req in requirement_paths:
|
|
386
|
+
with open(req, "r") as f:
|
|
387
|
+
dep_files.append(
|
|
388
|
+
DependencyFile(
|
|
389
|
+
file_path=req,
|
|
390
|
+
dependencies=self.parse_requirements(f.read()),
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
deps_to_scan = [d for d_file in dep_files for d in d_file.dependencies]
|
|
394
|
+
results = self.scan_dependencies(deps_to_scan, rules, callback)
|
|
395
|
+
return dep_files, results
|
|
396
|
+
except Exception as e:
|
|
397
|
+
log.error(f"Error while scanning. Received {e}")
|
|
398
|
+
raise e
|
|
399
|
+
|
|
400
|
+
@abstractmethod
|
|
401
|
+
def parse_requirements(self, raw_requirements: str) -> List[Dependency]:
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
@abstractmethod
|
|
405
|
+
def find_requirements(
|
|
406
|
+
self,
|
|
407
|
+
directory: str,
|
|
408
|
+
) -> list[str]: # returns paths of files
|
|
409
|
+
pass
|
guarddog/utils/archives.py
CHANGED
|
@@ -20,6 +20,7 @@ def is_supported_archive(path: str) -> bool:
|
|
|
20
20
|
bool: Represents the decision reached for the file
|
|
21
21
|
|
|
22
22
|
"""
|
|
23
|
+
|
|
23
24
|
def is_tar_archive(path: str) -> bool:
|
|
24
25
|
tar_exts = [".bz2", ".bzip2", ".gz", ".gzip", ".tgz", ".xz"]
|
|
25
26
|
|
|
@@ -68,7 +69,7 @@ def safe_extract(source_archive: str, target_directory: str) -> None:
|
|
|
68
69
|
recurse_add_perms(target_directory)
|
|
69
70
|
|
|
70
71
|
elif zipfile.is_zipfile(source_archive):
|
|
71
|
-
with zipfile.ZipFile(source_archive,
|
|
72
|
+
with zipfile.ZipFile(source_archive, "r") as zip:
|
|
72
73
|
for file in zip.namelist():
|
|
73
74
|
# Note: zip.extract cleans up any malicious file name
|
|
74
75
|
# such as directory traversal attempts This is not the
|
guarddog/utils/package_info.py
CHANGED
|
@@ -25,7 +25,9 @@ def get_package_info(name: str) -> dict:
|
|
|
25
25
|
|
|
26
26
|
# Check if package file exists
|
|
27
27
|
if response.status_code != 200:
|
|
28
|
-
raise Exception(
|
|
28
|
+
raise Exception(
|
|
29
|
+
"Received status code: " + str(response.status_code) + " from PyPI"
|
|
30
|
+
)
|
|
29
31
|
|
|
30
32
|
data = response.json()
|
|
31
33
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: guarddog
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary: GuardDog is a CLI tool
|
|
3
|
+
Version: 2.7.0
|
|
4
|
+
Summary: GuardDog is a CLI tool for identifying malicious open source packages
|
|
5
5
|
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: LICENSE-3rdparty.csv
|
|
8
|
+
License-File: NOTICE
|
|
6
9
|
Author: Ellen Wang
|
|
7
10
|
Requires-Python: >=3.10,<4
|
|
8
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
@@ -11,23 +14,21 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
11
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
18
|
Requires-Dist: click (>=8.1.3,<9.0.0)
|
|
15
|
-
Requires-Dist: click-option-group (>=0.5.5,<0.6.0)
|
|
16
|
-
Requires-Dist: colorama (>=0.4.6,<0.5.0)
|
|
17
19
|
Requires-Dist: configparser (>=5.3,<8.0)
|
|
18
|
-
Requires-Dist: disposable-email-domains (>=0.0.103,<0.0.
|
|
20
|
+
Requires-Dist: disposable-email-domains (>=0.0.103,<0.0.121)
|
|
19
21
|
Requires-Dist: prettytable (>=3.6.0,<4.0.0)
|
|
20
|
-
Requires-Dist: pygit2 (>=1.11,<1.
|
|
22
|
+
Requires-Dist: pygit2 (>=1.11,<1.19)
|
|
21
23
|
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|
|
22
24
|
Requires-Dist: python-whois (>=0.8,<0.10)
|
|
23
25
|
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
24
26
|
Requires-Dist: requests (>=2.29.0,<3.0.0)
|
|
25
27
|
Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
|
|
26
|
-
Requires-Dist: semgrep (
|
|
27
|
-
Requires-Dist: setuptools (>=70.3,<76.0)
|
|
28
|
+
Requires-Dist: semgrep (==1.121.0)
|
|
28
29
|
Requires-Dist: tarsafe (>=0.0.5,<0.0.6)
|
|
29
30
|
Requires-Dist: termcolor (>=2.1.0,<3.0.0)
|
|
30
|
-
Requires-Dist: urllib3 (
|
|
31
|
+
Requires-Dist: urllib3 (>=2.5.0,<3.0.0)
|
|
31
32
|
Requires-Dist: yara-python (>=4.5.1,<5.0.0)
|
|
32
33
|
Project-URL: Repository, https://github.com/DataDog/guarddog
|
|
33
34
|
Description-Content-Type: text/x-rst
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
guarddog/__init__.py,sha256=reb53KZG9b1nFmsDxj2fropaOceOCyM9bVMUdmZ2wS8,227
|
|
2
|
+
guarddog/__main__.py,sha256=GEdfW6I6g2c3H7bS0G43E4C-g7kXGUswzDCPFSwPgHY,246
|
|
3
|
+
guarddog/analyzer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
guarddog/analyzer/analyzer.py,sha256=eeWbazqGWFJBeT_OHHmEndH3hG_6PMxUajof6T9jutM,15413
|
|
5
|
+
guarddog/analyzer/metadata/__init__.py,sha256=tQTwWanifLsxfCdXIytPCO3chEIiTZ583uqKiQXQOog,855
|
|
6
|
+
guarddog/analyzer/metadata/bundled_binary.py,sha256=Tfgbc-exhbfvYjpgFH_Aa5KtT4ugJhrTV9SavZw1pHs,2594
|
|
7
|
+
guarddog/analyzer/metadata/deceptive_author.py,sha256=CLwntpSNBO4Bji3IEw2lctvNCMLiOxz0pG7KSEzIynM,2811
|
|
8
|
+
guarddog/analyzer/metadata/detector.py,sha256=dFhmoPVtxoed-Lz3Bd8GXZhorOVajvo5sIl9Iw8oCOM,641
|
|
9
|
+
guarddog/analyzer/metadata/empty_information.py,sha256=qswYDOrL3MxJ4VYq893Eiev5y56fgXrk5-CTKVXbkeA,1199
|
|
10
|
+
guarddog/analyzer/metadata/github_action/__init__.py,sha256=hOtiXKW-v5slzYW2M3k35M_YFfuLm8CNv5MwNSdFYMM,311
|
|
11
|
+
guarddog/analyzer/metadata/go/__init__.py,sha256=apwPnP9D4WEqgtR4RY0YIuFN7oNJXxJE_vYlp0ffRvQ,391
|
|
12
|
+
guarddog/analyzer/metadata/go/typosquatting.py,sha256=gU7VK5agC9T4xrWzliMHbcUOf2opw2MtLdMEf8n1_vI,4019
|
|
13
|
+
guarddog/analyzer/metadata/npm/__init__.py,sha256=j1Ng74bb1yD9XHFoYmJPzWL7vYMmLt6c2Lbc8lCqnUI,1326
|
|
14
|
+
guarddog/analyzer/metadata/npm/bundled_binary.py,sha256=vxJLhaTS7wymbktvmfJsF3whz3DWisjdD4wqHlNXvhg,392
|
|
15
|
+
guarddog/analyzer/metadata/npm/deceptive_author.py,sha256=RIBCWK3NjZiTf7tiz2V0ECy2Zr6Uwb69RQwIcWku380,366
|
|
16
|
+
guarddog/analyzer/metadata/npm/direct_url_dependency.py,sha256=hjjTLIT0UVudSf9A9Hory2OAqUkdxoXfKGXDgMtnNso,2449
|
|
17
|
+
guarddog/analyzer/metadata/npm/empty_information.py,sha256=Vpjr5Xe8JB4RIPzM0-BNmiqYiuY42LUbml7Yjig7Rcs,791
|
|
18
|
+
guarddog/analyzer/metadata/npm/npm_metadata_mismatch.py,sha256=Fj9MT7XlO2iXis4Da-_0CmM0weQiv8bVzKUoSm8ntYU,4428
|
|
19
|
+
guarddog/analyzer/metadata/npm/potentially_compromised_email_domain.py,sha256=Sm7fBfzayrbYXOpU5XzeCGKNfcX40hMOCSjKjhKwz-g,1719
|
|
20
|
+
guarddog/analyzer/metadata/npm/release_zero.py,sha256=FNHYfxl52i0V3HydccspcfF82T5L9d3ZyE-_J-UVoS0,633
|
|
21
|
+
guarddog/analyzer/metadata/npm/typosquatting.py,sha256=Roq7KeXO5P7DPtV9WivMhIgEXpBwoANr-nZo7VeGmCc,3146
|
|
22
|
+
guarddog/analyzer/metadata/npm/unclaimed_maintainer_email_domain.py,sha256=B8olfxXiaSM8c47dyftIgxmMVuwg4s7dMuVxrMS0GNE,953
|
|
23
|
+
guarddog/analyzer/metadata/npm/utils.py,sha256=QirjkoXhDcrGfoteh-V717TGq1xpXmvuC9dEp_5bt2s,454
|
|
24
|
+
guarddog/analyzer/metadata/potentially_compromised_email_domain.py,sha256=p2KCIByv4dBNM8h_1xPJgAOL197LLDvSLalZQpsvadg,2960
|
|
25
|
+
guarddog/analyzer/metadata/pypi/__init__.py,sha256=ef2tKnVzJPVy2eLgvBDII77t-zKOCPBOC6dmTCl_XBc,1381
|
|
26
|
+
guarddog/analyzer/metadata/pypi/bundled_binary.py,sha256=J5FqMYPTYnmb2MX9BFextVM5P0IzR522fhWV8TuTYMg,393
|
|
27
|
+
guarddog/analyzer/metadata/pypi/deceptive_author.py,sha256=KRbi7xfGYnEiq0p5HFazjV00a-3tIGe2ogXRvPhz9tI,367
|
|
28
|
+
guarddog/analyzer/metadata/pypi/empty_information.py,sha256=Ppaa_aEIlFJ5VpRtcm2ozlBoRHrTs6CWpPyh82sTM7g,814
|
|
29
|
+
guarddog/analyzer/metadata/pypi/potentially_compromised_email_domain.py,sha256=8zncGNsyfhWe04HRDF54rfP8y--_6l9ze-F-gH97pWo,1774
|
|
30
|
+
guarddog/analyzer/metadata/pypi/release_zero.py,sha256=UNUAFeB34B88N0LRe-tnmoZ3Y4x4AVZbe7m0i9Q3W7U,789
|
|
31
|
+
guarddog/analyzer/metadata/pypi/repository_integrity_mismatch.py,sha256=RB-Wf5Cbuh8KswkcCCHk7BVWvmOJccdHyb4mBjxiLhk,11903
|
|
32
|
+
guarddog/analyzer/metadata/pypi/single_python_file.py,sha256=L-YlmlP1TYA9XBeTHBPfECWgVIPTenUWRRnqwCMyh-o,1402
|
|
33
|
+
guarddog/analyzer/metadata/pypi/typosquatting.py,sha256=hqCBmmKL3_hp36iqsLYYYpqEQZYPQdFcAEpGn5b1F8w,4858
|
|
34
|
+
guarddog/analyzer/metadata/pypi/unclaimed_maintainer_email_domain.py,sha256=zfT0qTyN83O-7Yevc6tYIjmCAgh8Y_dhE6oZlKdNmm0,421
|
|
35
|
+
guarddog/analyzer/metadata/pypi/utils.py,sha256=UtG2JVep8bSOMz5LkrhXqS5Oy7Na19nHVct4IQMsQik,177
|
|
36
|
+
guarddog/analyzer/metadata/release_zero.py,sha256=F0I8coYivya7zns0XI1xNY4LLtTDQXoBUmze1wjwW4g,439
|
|
37
|
+
guarddog/analyzer/metadata/repository_integrity_mismatch.py,sha256=WGvck9JV2iCB8xaOc6X5cEGiu59juSWFfwbf6uEah3E,792
|
|
38
|
+
guarddog/analyzer/metadata/resources/placeholder_email_domains.txt,sha256=o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUc,11
|
|
39
|
+
guarddog/analyzer/metadata/resources/top_go_packages.json,sha256=HHOTcuWTGqlpXDOUgF7ejgmr8sGF_T5l7NQYdXmHcKQ,104044
|
|
40
|
+
guarddog/analyzer/metadata/resources/top_npm_packages.json,sha256=eeqVkFNW8ltYcGbjAJBzZrdxBEKezxa6AVVYoEpFazs,192960
|
|
41
|
+
guarddog/analyzer/metadata/resources/top_pypi_packages.json,sha256=7tN8yUTqbpq3HvNePK9IKrTIEeYblTMHXhUzyOdVN-w,1479906
|
|
42
|
+
guarddog/analyzer/metadata/typosquatting.py,sha256=6xmqsB0oKwrsXfTJ9DE1z8-nUW8ukgk1ZSS32iJmapk,4587
|
|
43
|
+
guarddog/analyzer/metadata/unclaimed_maintainer_email_domain.py,sha256=e-K9mSdph3y33fP_W7LNOlC7AnUVk28a1VfQJtG9vxo,2375
|
|
44
|
+
guarddog/analyzer/metadata/utils.py,sha256=bOrkELPza4ScUx1DfQxlqU-9DQeA5weISF42c0QCtls,1768
|
|
45
|
+
guarddog/analyzer/sourcecode/__init__.py,sha256=031VVi-DqTHxeYiI2mf2AuI76GaNUUyra_t0pRVqo5k,4631
|
|
46
|
+
guarddog/analyzer/sourcecode/api-obfuscation.yml,sha256=y_m6PSh8CfF6nxMulj2Yx4XYxS1T5OO0Eos9WJiDR74,2137
|
|
47
|
+
guarddog/analyzer/sourcecode/clipboard-access.yml,sha256=B36E7xKtAVgwZ29UWtvZa1AJcyfrhvehbLo6tlJqffk,524
|
|
48
|
+
guarddog/analyzer/sourcecode/cmd-overwrite.yml,sha256=l-tE3_G-LqCuCZnHab6v0PpCdMpoHPutBYcijeMZEA0,682
|
|
49
|
+
guarddog/analyzer/sourcecode/code-execution.yml,sha256=YyWBMhcXGzrM6JbXzRTBBFCs7bnoNGPFJJ_FIV2OYtc,5028
|
|
50
|
+
guarddog/analyzer/sourcecode/dll-hijacking.yml,sha256=_lkYp0P4545aiGazC5lRBeCcMHhGWzaK-95zu5MfRLY,3721
|
|
51
|
+
guarddog/analyzer/sourcecode/download-executable.yml,sha256=VuSNkpVh3DxHG7wfep3eAErGsOY9EL_268sNULYbfW4,3361
|
|
52
|
+
guarddog/analyzer/sourcecode/exec-base64.yml,sha256=Wg1jI_ff9I58Xq8gt8wXOQMrwHcPnzkAPyAURxnKHgw,2371
|
|
53
|
+
guarddog/analyzer/sourcecode/exfiltrate-sensitive-data.yml,sha256=hUxQEsJ4qF_25oMF8pdzAFOzq59m6k28WKz280uyaMg,2264
|
|
54
|
+
guarddog/analyzer/sourcecode/go-exec-base64.yml,sha256=Y5TUfLrmU1e5FTYW2zRKwn8yluBARHSXPr6Mr5vMVOY,1554
|
|
55
|
+
guarddog/analyzer/sourcecode/go-exec-download.yml,sha256=ZaZOvn3Xojsd2m8MQGLW1H7p28bPdpEbmDd37q2ZiX4,2931
|
|
56
|
+
guarddog/analyzer/sourcecode/go-exfiltrate-sensitive-data.yml,sha256=sb5GI-523zgE1nxNCrnRVjBSeOp7IfPy7qTQPBJMkco,3697
|
|
57
|
+
guarddog/analyzer/sourcecode/npm-dll-hijacking.yml,sha256=1TvI6UtCGCOMy4Ii-kM_oICYbMRGeOYdgXrG7-zmJ_Y,3460
|
|
58
|
+
guarddog/analyzer/sourcecode/npm-exec-base64.yml,sha256=zc5w2FTlHoZ7ot1flzlmYBkQu1I8eG1E63S5Aki7Goc,814
|
|
59
|
+
guarddog/analyzer/sourcecode/npm-exfiltrate-sensitive-data.yml,sha256=UYWXdkAab-dg_6UwVjiauHmy-9nlKiF86qcyxAwUoXg,3488
|
|
60
|
+
guarddog/analyzer/sourcecode/npm-install-script.yml,sha256=6BLe_V0SGEi1C79Y-FEIcMYHl4vLOOz8bLPrCU5jre8,1329
|
|
61
|
+
guarddog/analyzer/sourcecode/npm-obfuscation.yml,sha256=UxR5ezKr9sFcXEh2JKa20IYqq25J0JDfje82O3jUYMg,2174
|
|
62
|
+
guarddog/analyzer/sourcecode/npm-serialize-environment.yml,sha256=gFpr58INp44ZwxYZlIHyzpOgbVMDLv1ZRPTGAczX5dw,835
|
|
63
|
+
guarddog/analyzer/sourcecode/npm-silent-process-execution.yml,sha256=qnJHGesNPNpxGa8n2kQMpttLGck-6vZjI_SsweDyk7M,3513
|
|
64
|
+
guarddog/analyzer/sourcecode/npm-steganography.yml,sha256=XH0udcriAQq_6WOHAG4TpIedw8GgKyWx9gsG_Q_Fki8,915
|
|
65
|
+
guarddog/analyzer/sourcecode/obfuscation.yml,sha256=dp0BeCYShcTS8QiijSa9U53r6jkCjrFBW5jjNVoXdUU,1224
|
|
66
|
+
guarddog/analyzer/sourcecode/shady-links.yml,sha256=uDYVWDh0u20oy2zbXTJns64lvrQzLi95CLWgnftvX6Y,3222
|
|
67
|
+
guarddog/analyzer/sourcecode/silent-process-execution.yml,sha256=b6RjenMv7si7lXGak3uMmD7PMtQRuKPeJFggPW6UDNI,418
|
|
68
|
+
guarddog/analyzer/sourcecode/steganography.yml,sha256=3ceO6SJhu4XpZEjfwelLdOxeZ4Ho1OgUjbcacwtOhR0,606
|
|
69
|
+
guarddog/analyzer/sourcecode/suspicious_passwd_access_linux.yar,sha256=kplidsJ-ctg6W58VlYtLq10saZbcD1pm5_Xh4sqmHwk,422
|
|
70
|
+
guarddog/analyzer/sourcecode/unicode.yml,sha256=7fAygEtYwJ1iNKsyCjmLAEu15CLMWApfWXx_t_W3sOA,5596
|
|
71
|
+
guarddog/cli.py,sha256=Pk4WUD5a_TlPRpq2G4v_6FDGWu8IriXQPQ_ft8RXm5o,10692
|
|
72
|
+
guarddog/ecosystems.py,sha256=I1XPAhPuv7OnfZT3z0xcgEecUY1tFJdrklV07sMYffg,582
|
|
73
|
+
guarddog/reporters/__init__.py,sha256=lHNa5ZDsaIpjzS7SmheD5_GGAimGitXU-DNk-Wn97bI,749
|
|
74
|
+
guarddog/reporters/human_readable.py,sha256=WEyjOPdBE8adxC-tdFgwxcyDijsppLk4gIiZOUO69O0,4548
|
|
75
|
+
guarddog/reporters/json.py,sha256=gpbucxGoXBA6s7fNRzhQwZ4P6gWyz7BowsmQrnm4x6U,802
|
|
76
|
+
guarddog/reporters/reporter_factory.py,sha256=JUagC2UFkN2TZGpZIkI1MwMHEbwT9Ja1goQP95-k9SM,1465
|
|
77
|
+
guarddog/reporters/sarif.py,sha256=diOHJcN3CkSBxBDDg6l9DiZ3ebtUNCw0Rwd7QxCpM9k,7691
|
|
78
|
+
guarddog/scanners/__init__.py,sha256=dyBzyKANxTQvyd-oTjgm43gPwRMqB80eOzZ6UFNOuO8,2157
|
|
79
|
+
guarddog/scanners/extension_scanner.py,sha256=YdZ7Ai4U-MC83RJcnoo63m_aylAf3VnylwgvhGK3ktU,4915
|
|
80
|
+
guarddog/scanners/github_action_project_scanner.py,sha256=ISoBqUurwN0lMBtXwcNoalo3ghlbOJkZs9vSNZOT0kk,4216
|
|
81
|
+
guarddog/scanners/github_action_scanner.py,sha256=6lriTel3U7vNmCWBf0SWti9sLCv88RPlP8SVoAgpKJs,1781
|
|
82
|
+
guarddog/scanners/go_package_scanner.py,sha256=OdCbwtjJow9AxEv34z7WBfgTamqKj5DxJh7dly_1NuY,2926
|
|
83
|
+
guarddog/scanners/go_project_scanner.py,sha256=2suZJWvYBhiiBMIQXs38SR04E_Ast50jO44X27gEG10,3349
|
|
84
|
+
guarddog/scanners/npm_package_scanner.py,sha256=ciOvpRViMIQvNFupe5-hdXv65QLU5ObmacRkR2pgp18,2056
|
|
85
|
+
guarddog/scanners/npm_project_scanner.py,sha256=liz5Fyscab53IiSPg0T21Z0vT5eotcHPc_W5Xam4A88,4957
|
|
86
|
+
guarddog/scanners/pypi_package_scanner.py,sha256=ZkuRRbNejnpfFpIHJJ42GH34khiG8CUKWEPvVh_M_uk,2449
|
|
87
|
+
guarddog/scanners/pypi_project_scanner.py,sha256=O91c1UP2iZju84_N7cSE7pWGrY6rKapeUqXEVyKld3A,6435
|
|
88
|
+
guarddog/scanners/scanner.py,sha256=F7FhN-BQWtcTvh_gdhvj-rXYLMslzeTNxPbJsw1he2s,13695
|
|
89
|
+
guarddog/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
+
guarddog/utils/archives.py,sha256=OJBmRI3Gnr01_dUddI3MuTjXXlEyIjj3aLwffxBfMEc,2594
|
|
91
|
+
guarddog/utils/config.py,sha256=Msz7altsmNKry0vBPtL2BJ_VdBXsBFZX5ksLvXc2ix4,1403
|
|
92
|
+
guarddog/utils/exceptions.py,sha256=23Kzl3exqYK6X-bcGUeb8wPmSglWNX3GIDPkJ6lQzo4,54
|
|
93
|
+
guarddog/utils/package_info.py,sha256=6fHJPeLn6-tHhKHw0Soedfv2ruPd8zhW2kbhlc3Aem0,975
|
|
94
|
+
guarddog-2.7.0.dist-info/METADATA,sha256=GivoRFtvZAPGs4kqFkSePkEvpLgm87j4rqkA7DDz96I,1439
|
|
95
|
+
guarddog-2.7.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
96
|
+
guarddog-2.7.0.dist-info/entry_points.txt,sha256=vX2fvhnNdkbEL4pDzrH2NqjWVxeOaEYi0sJYmNgS2-s,45
|
|
97
|
+
guarddog-2.7.0.dist-info/licenses/LICENSE,sha256=w1aNZxHyoyOPJ4fSdiyrr06tCJZbTjCsH9K1uqeDVyU,11377
|
|
98
|
+
guarddog-2.7.0.dist-info/licenses/LICENSE-3rdparty.csv,sha256=cS61ONZL_xlXaTMvQXyBEi3J3es-40Gg6G-6idoa5Qk,314
|
|
99
|
+
guarddog-2.7.0.dist-info/licenses/NOTICE,sha256=nlyNt2IjG8IBoQkb7n6jszwAvmREpKAx0POzFO1s2JM,140
|
|
100
|
+
guarddog-2.7.0.dist-info/RECORD,,
|