klyrek-js 0.1.0__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.
- klyrek_js-0.1.0/.gitignore +20 -0
- klyrek_js-0.1.0/PKG-INFO +44 -0
- klyrek_js-0.1.0/README.md +24 -0
- klyrek_js-0.1.0/pyproject.toml +29 -0
- klyrek_js-0.1.0/src/klyrek_js/__init__.py +18 -0
- klyrek_js-0.1.0/src/klyrek_js/analyze.py +25 -0
- klyrek_js-0.1.0/src/klyrek_js/endpoints.py +47 -0
- klyrek_js-0.1.0/src/klyrek_js/py.typed +0 -0
- klyrek_js-0.1.0/src/klyrek_js/secrets.py +60 -0
- klyrek_js-0.1.0/src/klyrek_js/sourcemap.py +33 -0
- klyrek_js-0.1.0/tests/test_analyze.py +21 -0
- klyrek_js-0.1.0/tests/test_endpoints.py +49 -0
- klyrek_js-0.1.0/tests/test_secrets.py +58 -0
- klyrek_js-0.1.0/tests/test_sourcemap.py +37 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.eggs/
|
|
5
|
+
.qodo/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.env
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov/
|
|
16
|
+
*.log
|
|
17
|
+
.idea/
|
|
18
|
+
.vscode/
|
|
19
|
+
!.vscode/extensions.json
|
|
20
|
+
klyrek_output/
|
klyrek_js-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: klyrek-js
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: JavaScript analysis for the Klyrek ecosystem
|
|
5
|
+
Author: Klyrek Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: appsec,javascript,pentesting,reconnaissance,security
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Information Technology
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Security
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: klyrek-core
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# klyrek-js
|
|
22
|
+
|
|
23
|
+
JavaScript analysis. Pure text analysis — takes an already-fetched JS source string (from
|
|
24
|
+
`klyrek-http`/`klyrek-crawler`) rather than making its own requests, so this package has no
|
|
25
|
+
network dependency at all.
|
|
26
|
+
|
|
27
|
+
- **`endpoints`** — extracts API-shaped path and URL string literals referenced in the source
|
|
28
|
+
(`"/api/v1/users"`, `"https://api.target.com/graphql"`) as `Endpoint` records.
|
|
29
|
+
- **`secrets`** — regex-based detection of hardcoded credentials (AWS keys, Stripe live keys,
|
|
30
|
+
Slack tokens, GitHub PATs, private key blocks, generic `api_key`/`secret` assignments), as
|
|
31
|
+
`Finding` records with the matched value masked (`sk_l...3f9a`) rather than stored in full.
|
|
32
|
+
- **`sourcemap`** — detects a `//# sourceMappingURL=` reference, which (if the `.map` file is
|
|
33
|
+
publicly reachable) can reconstruct original unminified source.
|
|
34
|
+
|
|
35
|
+
Third-party SDK/library detection (Stripe.js, Sentry, Google Analytics, ...) deliberately isn't
|
|
36
|
+
duplicated here — `klyrek-tech`'s signature-based `fingerprint()` already works on any response
|
|
37
|
+
body, JS included.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from klyrek_js.analyze import analyze_js
|
|
41
|
+
|
|
42
|
+
result = analyze_js(js_source, js_url="https://target.com/static/app.js")
|
|
43
|
+
print(result.endpoints, result.findings)
|
|
44
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# klyrek-js
|
|
2
|
+
|
|
3
|
+
JavaScript analysis. Pure text analysis — takes an already-fetched JS source string (from
|
|
4
|
+
`klyrek-http`/`klyrek-crawler`) rather than making its own requests, so this package has no
|
|
5
|
+
network dependency at all.
|
|
6
|
+
|
|
7
|
+
- **`endpoints`** — extracts API-shaped path and URL string literals referenced in the source
|
|
8
|
+
(`"/api/v1/users"`, `"https://api.target.com/graphql"`) as `Endpoint` records.
|
|
9
|
+
- **`secrets`** — regex-based detection of hardcoded credentials (AWS keys, Stripe live keys,
|
|
10
|
+
Slack tokens, GitHub PATs, private key blocks, generic `api_key`/`secret` assignments), as
|
|
11
|
+
`Finding` records with the matched value masked (`sk_l...3f9a`) rather than stored in full.
|
|
12
|
+
- **`sourcemap`** — detects a `//# sourceMappingURL=` reference, which (if the `.map` file is
|
|
13
|
+
publicly reachable) can reconstruct original unminified source.
|
|
14
|
+
|
|
15
|
+
Third-party SDK/library detection (Stripe.js, Sentry, Google Analytics, ...) deliberately isn't
|
|
16
|
+
duplicated here — `klyrek-tech`'s signature-based `fingerprint()` already works on any response
|
|
17
|
+
body, JS included.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from klyrek_js.analyze import analyze_js
|
|
21
|
+
|
|
22
|
+
result = analyze_js(js_source, js_url="https://target.com/static/app.js")
|
|
23
|
+
print(result.endpoints, result.findings)
|
|
24
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "klyrek-js"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "JavaScript analysis for the Klyrek ecosystem"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Klyrek Contributors" }]
|
|
13
|
+
keywords = ["security", "reconnaissance", "appsec", "pentesting", "javascript"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Information Technology",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"klyrek-core",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pytest>=8.0", "ruff>=0.6", "mypy>=1.10"]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/klyrek_js"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""klyrek-js: JavaScript analysis (endpoint extraction, secret scanning, source maps)."""
|
|
2
|
+
|
|
3
|
+
from klyrek_js.analyze import JsAnalysis, analyze_js
|
|
4
|
+
from klyrek_js.endpoints import extract_endpoints
|
|
5
|
+
from klyrek_js.secrets import scan_for_secrets
|
|
6
|
+
from klyrek_js.sourcemap import find_source_map_url, source_map_finding
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"JsAnalysis",
|
|
12
|
+
"__version__",
|
|
13
|
+
"analyze_js",
|
|
14
|
+
"extract_endpoints",
|
|
15
|
+
"find_source_map_url",
|
|
16
|
+
"scan_for_secrets",
|
|
17
|
+
"source_map_finding",
|
|
18
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Run every JS analysis (endpoints, secrets, source maps) against one file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from klyrek_core.models import Endpoint, Finding
|
|
8
|
+
from klyrek_js.endpoints import extract_endpoints
|
|
9
|
+
from klyrek_js.secrets import scan_for_secrets
|
|
10
|
+
from klyrek_js.sourcemap import source_map_finding
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class JsAnalysis:
|
|
15
|
+
endpoints: list[Endpoint] = field(default_factory=list)
|
|
16
|
+
findings: list[Finding] = field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def analyze_js(js_source: str, js_url: str) -> JsAnalysis:
|
|
20
|
+
"""Extract endpoints, hardcoded secrets, and source map exposure from a JS file."""
|
|
21
|
+
findings = scan_for_secrets(js_source, source_url=js_url)
|
|
22
|
+
map_finding = source_map_finding(js_source, js_url)
|
|
23
|
+
if map_finding is not None:
|
|
24
|
+
findings.append(map_finding)
|
|
25
|
+
return JsAnalysis(endpoints=extract_endpoints(js_source, js_url), findings=findings)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Extract API-like endpoint strings referenced inside JavaScript source."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from urllib.parse import urljoin, urlsplit
|
|
7
|
+
|
|
8
|
+
from klyrek_core.models import Endpoint
|
|
9
|
+
|
|
10
|
+
# A quoted string literal that looks like an absolute API path, e.g. "/api/v1/users",
|
|
11
|
+
# '/graphql'. Anchored to a small set of API-shaped prefixes so it doesn't match every
|
|
12
|
+
# relative asset path like "/favicon.ico".
|
|
13
|
+
_PATH_LITERAL = re.compile(r"""["'](/(?:api|v\d+|graphql|rest)[a-zA-Z0-9_\-/{}.]*)["']""")
|
|
14
|
+
|
|
15
|
+
# Full absolute URLs referenced as string literals.
|
|
16
|
+
_URL_LITERAL = re.compile(r"""["'](https?://[^\s"'<>]+)["']""")
|
|
17
|
+
|
|
18
|
+
_API_PATH_HINTS = ("/api", "/graphql", "/rest", "/v1", "/v2", "/v3")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _looks_like_api_url(url: str) -> bool:
|
|
22
|
+
path = urlsplit(url).path.lower()
|
|
23
|
+
return any(hint in path for hint in _API_PATH_HINTS)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def extract_endpoints(js_source: str, base_url: str) -> list[Endpoint]:
|
|
27
|
+
"""Pull API-shaped path and URL string literals out of a JavaScript source file."""
|
|
28
|
+
seen: set[str] = set()
|
|
29
|
+
endpoints: list[Endpoint] = []
|
|
30
|
+
|
|
31
|
+
for match in _PATH_LITERAL.finditer(js_source):
|
|
32
|
+
url = urljoin(base_url, match.group(1))
|
|
33
|
+
if url not in seen:
|
|
34
|
+
seen.add(url)
|
|
35
|
+
endpoints.append(
|
|
36
|
+
Endpoint(url=url, method="GET", source="js-endpoint", discovered_by="klyrek-js")
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
for match in _URL_LITERAL.finditer(js_source):
|
|
40
|
+
url = match.group(1)
|
|
41
|
+
if url not in seen and _looks_like_api_url(url):
|
|
42
|
+
seen.add(url)
|
|
43
|
+
endpoints.append(
|
|
44
|
+
Endpoint(url=url, method="GET", source="js-endpoint", discovered_by="klyrek-js")
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return endpoints
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Detect hardcoded secrets and API keys embedded in JavaScript source."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from klyrek_core.models import Finding, Severity
|
|
8
|
+
|
|
9
|
+
_GENERIC_ASSIGNMENT = (
|
|
10
|
+
r"(?i)(?:api[_-]?key|secret[_-]?key|access[_-]?token|client[_-]?secret)"
|
|
11
|
+
r"""\s*[:=]\s*["'][A-Za-z0-9_\-]{16,}["']"""
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_SECRET_PATTERNS: list[tuple[str, str, Severity]] = [
|
|
15
|
+
("AWS Access Key ID", r"\bAKIA[0-9A-Z]{16}\b", Severity.HIGH),
|
|
16
|
+
("Google API Key", r"\bAIza[0-9A-Za-z\-_]{35}\b", Severity.HIGH),
|
|
17
|
+
("Stripe Live Secret Key", r"\bsk_live_[0-9a-zA-Z]{16,}\b", Severity.CRITICAL),
|
|
18
|
+
("Stripe Live Publishable Key", r"\bpk_live_[0-9a-zA-Z]{16,}\b", Severity.LOW),
|
|
19
|
+
("Slack Token", r"\bxox[baprs]-[0-9A-Za-z\-]{10,}\b", Severity.HIGH),
|
|
20
|
+
("GitHub Personal Access Token", r"\bgh[pousr]_[0-9A-Za-z]{36}\b", Severity.HIGH),
|
|
21
|
+
(
|
|
22
|
+
"Private Key Block",
|
|
23
|
+
r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",
|
|
24
|
+
Severity.CRITICAL,
|
|
25
|
+
),
|
|
26
|
+
("Generic API key/secret assignment", _GENERIC_ASSIGNMENT, Severity.MEDIUM),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
_COMPILED = [(name, re.compile(pattern), severity) for name, pattern, severity in _SECRET_PATTERNS]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _mask(value: str) -> str:
|
|
33
|
+
if len(value) <= 8:
|
|
34
|
+
return "*" * len(value)
|
|
35
|
+
return f"{value[:4]}...{value[-4:]}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def scan_for_secrets(js_source: str, source_url: str | None = None) -> list[Finding]:
|
|
39
|
+
"""Scan JavaScript source text for hardcoded secrets and API keys.
|
|
40
|
+
|
|
41
|
+
Matched values are masked in the evidence (kept to first/last 4 chars) rather than
|
|
42
|
+
stored in full, since a Finding may end up in a report handed to a third party.
|
|
43
|
+
"""
|
|
44
|
+
findings: list[Finding] = []
|
|
45
|
+
for name, pattern, severity in _COMPILED:
|
|
46
|
+
for match in pattern.finditer(js_source):
|
|
47
|
+
evidence = [_mask(match.group(0))]
|
|
48
|
+
if source_url:
|
|
49
|
+
evidence.append(source_url)
|
|
50
|
+
findings.append(
|
|
51
|
+
Finding(
|
|
52
|
+
title=f"Hardcoded {name} found in JavaScript",
|
|
53
|
+
severity=severity,
|
|
54
|
+
description=f"A pattern matching a {name} was found embedded in "
|
|
55
|
+
"client-side JavaScript, which is downloadable by anyone visiting the page.",
|
|
56
|
+
evidence=evidence,
|
|
57
|
+
module="klyrek-js",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
return findings
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Detect an exposed source map referenced by a JavaScript file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from urllib.parse import urljoin
|
|
7
|
+
|
|
8
|
+
from klyrek_core.models import Finding, Severity
|
|
9
|
+
|
|
10
|
+
_SOURCE_MAP_COMMENT = re.compile(r"//[#@]\s*sourceMappingURL=(\S+)")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_source_map_url(js_source: str, js_url: str) -> str | None:
|
|
14
|
+
"""Return the absolute URL of a referenced source map, if the JS file has one."""
|
|
15
|
+
match = _SOURCE_MAP_COMMENT.search(js_source)
|
|
16
|
+
if not match:
|
|
17
|
+
return None
|
|
18
|
+
return urljoin(js_url, match.group(1))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def source_map_finding(js_source: str, js_url: str) -> Finding | None:
|
|
22
|
+
"""Flag an exposed source map reference as a Finding, if one is present."""
|
|
23
|
+
map_url = find_source_map_url(js_source, js_url)
|
|
24
|
+
if map_url is None:
|
|
25
|
+
return None
|
|
26
|
+
return Finding(
|
|
27
|
+
title="JavaScript source map exposed",
|
|
28
|
+
severity=Severity.LOW,
|
|
29
|
+
description="This script references a source map, which (if publicly reachable) can "
|
|
30
|
+
"reconstruct original, unminified source code including comments and file layout.",
|
|
31
|
+
evidence=[f"{js_url} -> {map_url}"],
|
|
32
|
+
module="klyrek-js",
|
|
33
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from klyrek_js.analyze import analyze_js
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_analyze_combines_endpoints_and_findings():
|
|
5
|
+
js = """
|
|
6
|
+
fetch("/api/v1/users");
|
|
7
|
+
const key = "AKIAIOSFODNN7EXAMPLE";
|
|
8
|
+
//# sourceMappingURL=app.js.map
|
|
9
|
+
"""
|
|
10
|
+
result = analyze_js(js, "https://target.com/app.js")
|
|
11
|
+
|
|
12
|
+
assert any(e.url == "https://target.com/api/v1/users" for e in result.endpoints)
|
|
13
|
+
assert any("AWS Access Key" in f.title for f in result.findings)
|
|
14
|
+
assert any("source map" in f.title.lower() for f in result.findings)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_analyze_clean_file_has_no_findings_or_endpoints():
|
|
18
|
+
js = "function greet(name) { return 'hi ' + name; }"
|
|
19
|
+
result = analyze_js(js, "https://target.com/app.js")
|
|
20
|
+
assert result.endpoints == []
|
|
21
|
+
assert result.findings == []
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from klyrek_js.endpoints import extract_endpoints
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_relative_api_path_is_extracted():
|
|
5
|
+
js = 'fetch("/api/v1/users").then(r => r.json());'
|
|
6
|
+
endpoints = extract_endpoints(js, "https://target.com/app.js")
|
|
7
|
+
assert any(e.url == "https://target.com/api/v1/users" for e in endpoints)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_graphql_path_is_extracted():
|
|
11
|
+
js = "const client = new ApolloClient({ uri: '/graphql' });"
|
|
12
|
+
endpoints = extract_endpoints(js, "https://target.com/app.js")
|
|
13
|
+
assert any(e.url == "https://target.com/graphql" for e in endpoints)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_absolute_api_url_is_extracted():
|
|
17
|
+
js = 'axios.get("https://api.target.com/v2/orders");'
|
|
18
|
+
endpoints = extract_endpoints(js, "https://target.com/app.js")
|
|
19
|
+
assert any(e.url == "https://api.target.com/v2/orders" for e in endpoints)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_absolute_non_api_url_is_ignored():
|
|
23
|
+
js = 'const img = "https://cdn.target.com/logo.png";'
|
|
24
|
+
endpoints = extract_endpoints(js, "https://target.com/app.js")
|
|
25
|
+
assert endpoints == []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_plain_asset_path_is_not_extracted():
|
|
29
|
+
js = 'const icon = "/favicon.ico";'
|
|
30
|
+
endpoints = extract_endpoints(js, "https://target.com/app.js")
|
|
31
|
+
assert endpoints == []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_duplicate_paths_are_deduplicated():
|
|
35
|
+
js = '"/api/v1/users" and again "/api/v1/users"'
|
|
36
|
+
endpoints = extract_endpoints(js, "https://target.com/app.js")
|
|
37
|
+
assert len(endpoints) == 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_endpoints_are_tagged_with_source_and_discovered_by():
|
|
41
|
+
js = 'fetch("/api/v1/users");'
|
|
42
|
+
endpoints = extract_endpoints(js, "https://target.com/app.js")
|
|
43
|
+
assert endpoints[0].source == "js-endpoint"
|
|
44
|
+
assert endpoints[0].discovered_by == "klyrek-js"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_no_matches_on_plain_source():
|
|
48
|
+
js = "function greet() { console.log('hello world'); }"
|
|
49
|
+
assert extract_endpoints(js, "https://target.com/app.js") == []
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from klyrek_js.secrets import scan_for_secrets
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_aws_access_key_is_detected():
|
|
5
|
+
js = 'const key = "AKIAIOSFODNN7EXAMPLE";'
|
|
6
|
+
findings = scan_for_secrets(js)
|
|
7
|
+
assert any("AWS Access Key" in f.title for f in findings)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_stripe_live_secret_key_is_detected_as_critical():
|
|
11
|
+
js = 'stripe.setApiKey("sk_live_51H8x9K2eZvKYlo2C0000000000");'
|
|
12
|
+
findings = scan_for_secrets(js)
|
|
13
|
+
match = next(f for f in findings if "Stripe Live Secret Key" in f.title)
|
|
14
|
+
assert match.severity.value == "critical"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_stripe_publishable_key_is_low_severity():
|
|
18
|
+
js = 'stripe.setPublishableKey("pk_live_51H8x9K2eZvKYlo2C0000000000");'
|
|
19
|
+
findings = scan_for_secrets(js)
|
|
20
|
+
match = next(f for f in findings if "Publishable Key" in f.title)
|
|
21
|
+
assert match.severity.value == "low"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_private_key_block_is_detected():
|
|
25
|
+
js = "const key = `-----BEGIN RSA PRIVATE KEY-----\\nMIIEow...`;"
|
|
26
|
+
findings = scan_for_secrets(js)
|
|
27
|
+
assert any("Private Key Block" in f.title for f in findings)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_generic_api_key_assignment_is_detected():
|
|
31
|
+
js = 'const config = { apiKey: "abcdefghij1234567890" };'
|
|
32
|
+
findings = scan_for_secrets(js)
|
|
33
|
+
assert any("Generic API key" in f.title for f in findings)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_secret_value_is_masked_in_evidence():
|
|
37
|
+
js = 'const key = "AKIAIOSFODNN7EXAMPLE";'
|
|
38
|
+
findings = scan_for_secrets(js)
|
|
39
|
+
evidence = findings[0].evidence[0]
|
|
40
|
+
assert "AKIAIOSFODNN7EXAMPLE" not in evidence
|
|
41
|
+
assert evidence.startswith("AKIA")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_source_url_included_in_evidence_when_given():
|
|
45
|
+
js = 'const key = "AKIAIOSFODNN7EXAMPLE";'
|
|
46
|
+
findings = scan_for_secrets(js, source_url="https://target.com/app.js")
|
|
47
|
+
assert "https://target.com/app.js" in findings[0].evidence
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_no_secrets_in_clean_source():
|
|
51
|
+
js = "function add(a, b) { return a + b; }"
|
|
52
|
+
assert scan_for_secrets(js) == []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_all_findings_tagged_with_module():
|
|
56
|
+
js = 'const key = "AKIAIOSFODNN7EXAMPLE";'
|
|
57
|
+
findings = scan_for_secrets(js)
|
|
58
|
+
assert all(f.module == "klyrek-js" for f in findings)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from klyrek_js.sourcemap import find_source_map_url, source_map_finding
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_relative_source_map_url_is_resolved():
|
|
5
|
+
js = "console.log('hi');\n//# sourceMappingURL=app.js.map"
|
|
6
|
+
url = find_source_map_url(js, "https://target.com/static/app.js")
|
|
7
|
+
assert url == "https://target.com/static/app.js.map"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_at_sign_comment_style_is_supported():
|
|
11
|
+
js = "console.log('hi');\n//@ sourceMappingURL=app.js.map"
|
|
12
|
+
url = find_source_map_url(js, "https://target.com/static/app.js")
|
|
13
|
+
assert url == "https://target.com/static/app.js.map"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_absolute_source_map_url_is_used_as_is():
|
|
17
|
+
js = "//# sourceMappingURL=https://cdn.target.com/maps/app.js.map"
|
|
18
|
+
url = find_source_map_url(js, "https://target.com/static/app.js")
|
|
19
|
+
assert url == "https://cdn.target.com/maps/app.js.map"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_no_source_map_returns_none():
|
|
23
|
+
js = "console.log('no map here');"
|
|
24
|
+
assert find_source_map_url(js, "https://target.com/app.js") is None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_source_map_finding_is_low_severity():
|
|
28
|
+
js = "//# sourceMappingURL=app.js.map"
|
|
29
|
+
finding = source_map_finding(js, "https://target.com/app.js")
|
|
30
|
+
assert finding is not None
|
|
31
|
+
assert finding.severity.value == "low"
|
|
32
|
+
assert finding.module == "klyrek-js"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_no_finding_when_no_source_map():
|
|
36
|
+
js = "console.log('clean');"
|
|
37
|
+
assert source_map_finding(js, "https://target.com/app.js") is None
|