mpak-scanner 0.2.2__tar.gz → 0.2.4__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.
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/.gitignore +3 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/CLAUDE.md +36 -11
- mpak_scanner-0.2.4/Dockerfile +26 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/PKG-INFO +1 -1
- mpak_scanner-0.2.4/SCANNER_BUG_REPORT_NODE_AI05.md +157 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/pyproject.toml +4 -1
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/__init__.py +1 -1
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/artifact_integrity/ai01_manifest.py +1 -1
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/artifact_integrity/ai05_bundle_completeness.py +25 -2
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/supply_chain/sc02_vuln_scan.py +5 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/models.py +1 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/scanner.py +1 -1
- mpak_scanner-0.2.4/tests/test_e2e_bundles.py +144 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/test_scanner.py +87 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/uv.lock +1 -1
- mpak_scanner-0.2.2/Dockerfile +0 -4
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/Makefile +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/README.md +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/docs/CONTRIBUTING.md +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/docs/CONTROLS.md +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/docs/LEVELS.md +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/cli.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/__init__.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/artifact_integrity/__init__.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/artifact_integrity/ai02_content_hashes.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/artifact_integrity/ai03_bundle_signature.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/artifact_integrity/ai04_reproducible_build.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/base.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/capability_declaration/__init__.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/capability_declaration/cd01_tools.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/capability_declaration/cd02_permission_scope.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/capability_declaration/cd03_tool_description_safety.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/capability_declaration/cd04_credential_scope.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/capability_declaration/cd05_token_lifetime.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/code_quality/__init__.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/code_quality/cq01_secrets.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/code_quality/cq02_malicious.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/code_quality/cq03_static_analysis.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/code_quality/cq04_input_validation.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/code_quality/cq05_safe_execution.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/code_quality/cq06_behavioral_analysis.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/provenance/__init__.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/provenance/pr01_source_repository.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/provenance/pr02_author_identity.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/provenance/pr03_build_attestation.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/provenance/pr04_commit_linkage.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/provenance/pr05_repository_health.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/supply_chain/__init__.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/supply_chain/sc01_sbom.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/supply_chain/sc03_dependency_pinning.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/supply_chain/sc04_lockfile_integrity.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/supply_chain/sc05_trusted_sources.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/job.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/rules/__init__.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/rules/malicious.yaml +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/rules/secrets.yaml +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/rules/unsafe-exec.yaml +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/schemas.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/utils/__init__.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/utils/name_analysis.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/__init__.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/README.md +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/clean-l1-bundle/manifest.json +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/clean-l1-bundle/src/server.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/has-secrets-bundle/manifest.json +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/has-secrets-bundle/src/server.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/has-vulns-bundle/manifest.json +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/has-vulns-bundle/requirements.txt +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/has-vulns-bundle/src/server.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/invalid-manifest-bundle/manifest.json +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/invalid-manifest-bundle/src/server.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/malicious-test-bundle/manifest.json +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/malicious-test-bundle/src/malicious_server/__init__.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/malicious-test-bundle/src/malicious_server/server.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/malicious-test-bundle/src/malicious_server/setup_hook.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/missing-tools-bundle/manifest.json +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/missing-tools-bundle/src/server.py +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/node-server-bundle/manifest.json +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/node-server-bundle/src/server.js +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/unsafe-node-bundle/manifest.json +0 -0
- {mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/unsafe-node-bundle/src/server.js +0 -0
|
@@ -62,7 +62,9 @@ src/mpak_scanner/
|
|
|
62
62
|
└── capability_declaration/ # CD-01, CD-02, CD-03
|
|
63
63
|
|
|
64
64
|
tests/
|
|
65
|
-
├── test_scanner.py #
|
|
65
|
+
├── test_scanner.py # Unit + fixture-based integration tests
|
|
66
|
+
├── test_e2e_bundles.py # E2E tests against real registry bundles
|
|
67
|
+
├── data/ # Downloaded bundles for e2e tests (gitignored)
|
|
66
68
|
└── fixtures/ # Test bundles (see fixtures/README.md)
|
|
67
69
|
├── clean-l1-bundle/ # Passes Level 1
|
|
68
70
|
├── has-secrets-bundle/ # Fails CQ-01
|
|
@@ -128,35 +130,58 @@ Located in `tests/fixtures/`. Each fixture tests specific controls:
|
|
|
128
130
|
- `node-server-bundle/` - Clean Node.js bundle, passes CQ-05
|
|
129
131
|
- `unsafe-node-bundle/` - Contains unsafe patterns (CQ-05 should fail)
|
|
130
132
|
|
|
133
|
+
## E2E Tests
|
|
134
|
+
|
|
135
|
+
End-to-end tests scan real bundles pulled from the mpak registry. They catch false positives and regressions that synthetic fixtures miss.
|
|
136
|
+
|
|
137
|
+
### Setup (one-time)
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
mpak bundle pull @nimblebraininc/finnhub -o tests/data/finnhub.mcpb
|
|
141
|
+
mpak bundle pull @nimblebraininc/folk -o tests/data/folk.mcpb
|
|
142
|
+
mpak bundle pull @nimblebraininc/nationalparks -o tests/data/nationalparks.mcpb
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Bundles are stored in `tests/data/` (gitignored). Tests skip gracefully if bundles are missing.
|
|
146
|
+
|
|
147
|
+
### Running
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
uv run pytest -m e2e -v # e2e tests only
|
|
151
|
+
uv run pytest tests/test_scanner.py # unit tests only (fast)
|
|
152
|
+
uv run pytest # all tests (unit + e2e)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Adding new e2e bundles
|
|
156
|
+
|
|
157
|
+
1. `mpak bundle pull @scope/name -o tests/data/name.mcpb`
|
|
158
|
+
2. Add a `pytest.param()` entry to the appropriate list in `test_e2e_bundles.py`
|
|
159
|
+
|
|
131
160
|
## Releasing a New Version
|
|
132
161
|
|
|
133
162
|
The scanner is distributed via PyPI. The Docker image installs from PyPI, not local source.
|
|
134
163
|
|
|
135
164
|
### Steps
|
|
136
165
|
|
|
137
|
-
1. **Bump version** in
|
|
166
|
+
1. **Bump version** in four files (must all match):
|
|
138
167
|
- `pyproject.toml` (`version = "X.Y.Z"`)
|
|
139
168
|
- `src/mpak_scanner/__init__.py` (`__version__ = "X.Y.Z"`)
|
|
140
169
|
- `src/mpak_scanner/scanner.py` (`SCANNER_VERSION = "X.Y.Z"`)
|
|
170
|
+
- `Dockerfile` (`mpak-scanner[job]==X.Y.Z`)
|
|
141
171
|
|
|
142
|
-
2. **
|
|
143
|
-
```dockerfile
|
|
144
|
-
RUN pip install --no-cache-dir mpak-scanner==X.Y.Z
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
3. **Run verification**:
|
|
172
|
+
2. **Run verification**:
|
|
148
173
|
```bash
|
|
149
174
|
uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/ && uv run ty check src/ && uv run pytest
|
|
150
175
|
```
|
|
151
176
|
|
|
152
|
-
|
|
177
|
+
3. **Commit and push** in `apps/mpak`
|
|
153
178
|
|
|
154
|
-
|
|
179
|
+
4. **Publish to PyPI** (from `apps/mpak/apps/scanner/`):
|
|
155
180
|
```bash
|
|
156
181
|
uv build && uv publish
|
|
157
182
|
```
|
|
158
183
|
|
|
159
|
-
|
|
184
|
+
5. **Build + push Docker image** (from `hq/deployments/mpak/`):
|
|
160
185
|
```bash
|
|
161
186
|
make deploy-scanner ENV=production
|
|
162
187
|
make apply-scanner-infra ENV=production # only if RBAC/secrets changed
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
FROM python:3.13-slim
|
|
2
|
+
|
|
3
|
+
# System deps for external security tools
|
|
4
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
5
|
+
curl \
|
|
6
|
+
ca-certificates \
|
|
7
|
+
git \
|
|
8
|
+
nodejs \
|
|
9
|
+
npm \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
# Anchore tools: syft (SBOM), grype (CVE scanning)
|
|
13
|
+
RUN curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin && \
|
|
14
|
+
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
|
|
15
|
+
|
|
16
|
+
# TruffleHog (secret detection)
|
|
17
|
+
RUN curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin
|
|
18
|
+
|
|
19
|
+
# ESLint with security plugin (JS/TS static analysis)
|
|
20
|
+
RUN npm install -g eslint eslint-plugin-security --no-fund --no-audit
|
|
21
|
+
|
|
22
|
+
# mpak-scanner + Python security tools (bandit, guarddog)
|
|
23
|
+
RUN pip install --no-cache-dir "mpak-scanner[job]==0.2.4" bandit guarddog
|
|
24
|
+
|
|
25
|
+
ENTRYPOINT ["mpak-scanner"]
|
|
26
|
+
CMD ["job"]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# mpak-scanner: AI-05 false positives on Node.js bundles
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
AI-05 (Bundle Completeness) produces false-positive HIGH findings on every compiled JavaScript file in a Node.js bundle's output directory (`build/`, `dist/`). The control has Python-specific module resolution but no equivalent for Node.js, causing all non-entry-point `.js` files to be flagged as "unexpected executables." This blocks Node.js bundles from reaching L2 compliance.
|
|
6
|
+
|
|
7
|
+
**Scanner version**: 0.2.3
|
|
8
|
+
**Bundle tested**: `@nimblebraininc/nationalparks@0.2.0` (Node.js, TypeScript)
|
|
9
|
+
|
|
10
|
+
## Reproduction
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Build a Node.js MCPB bundle (any multi-file TypeScript server)
|
|
14
|
+
cd mcp-servers/nationalparks
|
|
15
|
+
npm ci && npm run build && npm prune --omit=dev
|
|
16
|
+
mcpb pack . test.mcpb
|
|
17
|
+
|
|
18
|
+
# Scan it
|
|
19
|
+
uvx mpak-scanner scan test.mcpb --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Result**: AI-05 FAIL with 11 HIGH findings. Every `.js` file in `build/` except the entry point is flagged:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
[HIGH] Unexpected executable: build/constants.js
|
|
26
|
+
[HIGH] Unexpected executable: build/formatters.js
|
|
27
|
+
[HIGH] Unexpected executable: build/config.js
|
|
28
|
+
[HIGH] Unexpected executable: build/schemas.js
|
|
29
|
+
[HIGH] Unexpected executable: build/utils/npsApiClient.js
|
|
30
|
+
[HIGH] Unexpected executable: build/handlers/getAlerts.js
|
|
31
|
+
[HIGH] Unexpected executable: build/handlers/getParkDetails.js
|
|
32
|
+
[HIGH] Unexpected executable: build/handlers/getEvents.js
|
|
33
|
+
[HIGH] Unexpected executable: build/handlers/findParks.js
|
|
34
|
+
[HIGH] Unexpected executable: build/handlers/getCampgrounds.js
|
|
35
|
+
[HIGH] Unexpected executable: build/handlers/getVisitorCenters.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Expected**: AI-05 PASS. These files are standard Node.js modules imported by the entry point (`build/index.js`).
|
|
39
|
+
|
|
40
|
+
## Root cause
|
|
41
|
+
|
|
42
|
+
`ai05_bundle_completeness.py` builds a "referenced files" set from the manifest, then flags any executable-extension file not in that set.
|
|
43
|
+
|
|
44
|
+
For **Python**, `_add_python_module_files()` (line 235) resolves `python -m mcp_echo.server` into the entire `mcp_echo/` package directory tree, adding all files as referenced. This correctly covers Python's module import pattern.
|
|
45
|
+
|
|
46
|
+
For **Node.js**, there is no equivalent. The referenced set only contains:
|
|
47
|
+
- `server.entry_point` (e.g., `build/index.js`) -- added at line 198
|
|
48
|
+
- `mcp_config.args` file-like values (e.g., `${__dirname}/build/index.js` cleaned to `build/index.js`) -- added at line 227
|
|
49
|
+
|
|
50
|
+
But `build/index.js` imports sibling modules (`./config.js`, `./schemas.js`, `./handlers/findParks.js`, etc.) via standard ES module `import` statements. These are never added to the referenced set.
|
|
51
|
+
|
|
52
|
+
### Code path
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
_build_referenced_files()
|
|
56
|
+
├── server.entry_point → "build/index.js" ✓ added
|
|
57
|
+
├── mcp_config.args → "${__dirname}/build/index.js" ✓ added (duplicate)
|
|
58
|
+
├── Python -m detection → _add_python_module_files() ✓ adds whole package
|
|
59
|
+
└── Node.js entry_point → (nothing) ✗ missing
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Why node_modules isn't affected
|
|
63
|
+
|
|
64
|
+
`node_modules/` is correctly allowed via `DEP_DIRS` on line 42 / `_is_in_dep_dir()` on line 296. The problem is limited to the server's own compiled output directory.
|
|
65
|
+
|
|
66
|
+
## Proposed fix
|
|
67
|
+
|
|
68
|
+
Add `_add_node_entry_point_files()` analogous to `_add_python_module_files()`:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
def _add_node_entry_point_files(
|
|
72
|
+
self, entry_point: str, bundle_dir: Path, referenced: set[str]
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Resolve a Node.js entry point to its sibling modules.
|
|
75
|
+
|
|
76
|
+
TypeScript compiles to a directory (build/, dist/) where the entry
|
|
77
|
+
point imports other .js files. Add all files in the entry point's
|
|
78
|
+
directory tree as referenced.
|
|
79
|
+
"""
|
|
80
|
+
entry_path = bundle_dir / entry_point
|
|
81
|
+
if not entry_path.exists():
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
# Add all files in the entry point's parent directory tree
|
|
85
|
+
entry_dir = entry_path.parent
|
|
86
|
+
if entry_dir == bundle_dir:
|
|
87
|
+
# Entry point is at root; don't add everything
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
for f in entry_dir.rglob("*"):
|
|
91
|
+
if f.is_file():
|
|
92
|
+
referenced.add(str(f.relative_to(bundle_dir)).replace("\\", "/"))
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then call it from `_build_referenced_files()` when `server.type == "node"`:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
# After the entry_point handling (around line 198):
|
|
99
|
+
server_type = server.get("type", "")
|
|
100
|
+
if server_type == "node" and entry_point and bundle_dir:
|
|
101
|
+
self._add_node_entry_point_files(entry_point, bundle_dir, referenced)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Why directory-tree resolution is correct
|
|
105
|
+
|
|
106
|
+
This mirrors the Python approach:
|
|
107
|
+
- **Python**: `-m mcp_echo.server` resolves to the `mcp_echo/` package, and all files in that package are added (line 251: `package_dir.rglob("*")`)
|
|
108
|
+
- **Node.js**: `build/index.js` resolves to the `build/` directory, and all files in that directory are added
|
|
109
|
+
|
|
110
|
+
Both languages compile/organize source into a directory where the entry point's siblings are expected runtime dependencies.
|
|
111
|
+
|
|
112
|
+
### Edge case: root-level entry point
|
|
113
|
+
|
|
114
|
+
If `entry_point` is `index.js` (at bundle root), we should NOT add every file in the bundle. The proposed code handles this with the `entry_dir == bundle_dir` guard.
|
|
115
|
+
|
|
116
|
+
## Test fixture gap
|
|
117
|
+
|
|
118
|
+
The existing `node-server-bundle` fixture only has a single file (`src/server.js`):
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
node-server-bundle/
|
|
122
|
+
├── manifest.json
|
|
123
|
+
└── src/server.js
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This doesn't represent real Node.js bundles which have multi-file compiled output. A realistic fixture would look like:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
node-server-bundle/
|
|
130
|
+
├── manifest.json
|
|
131
|
+
├── package.json
|
|
132
|
+
├── build/
|
|
133
|
+
│ ├── index.js # entry_point
|
|
134
|
+
│ ├── config.js # imported by index.js
|
|
135
|
+
│ └── handlers/
|
|
136
|
+
│ └── handler.js # imported by index.js
|
|
137
|
+
└── node_modules/
|
|
138
|
+
└── some-dep/
|
|
139
|
+
├── package.json
|
|
140
|
+
└── index.js
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Suggested test cases
|
|
144
|
+
|
|
145
|
+
1. **Multi-file Node.js build** (should PASS): Entry point at `build/index.js` with sibling modules in `build/`
|
|
146
|
+
2. **Node.js with stray script** (should FAIL): Entry point in `build/` plus an unrelated `scripts/deploy.sh` at root
|
|
147
|
+
3. **Root-level entry point** (should PASS): Entry point at `index.js` with no extra files outside node_modules
|
|
148
|
+
|
|
149
|
+
## Impact
|
|
150
|
+
|
|
151
|
+
- Blocks all multi-file Node.js servers from L2 compliance
|
|
152
|
+
- `@nimblebraininc/nationalparks` is the first Node.js server going through MCPB migration
|
|
153
|
+
- All 15 other L2 controls pass; AI-05 is the sole blocker
|
|
154
|
+
|
|
155
|
+
## Related: mcpb CLI excludes lockfiles
|
|
156
|
+
|
|
157
|
+
Separate issue, but noted here for context: `mcpb pack` v2.1.2 hardcodes `package-lock.json` in its `EXCLUDE_PATTERNS` (`dist/node/files.js`). This prevents lockfiles from appearing in Node.js bundles, which causes SC-03/SC-04 INFO findings about missing lockfiles. SC-03 and SC-04 still PASS (SC-03 falls back to package.json parsing; SC-04 is client-enforced), but the lockfile should ideally be includable. This is an `mcpb` CLI issue, not a scanner issue.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mpak-scanner"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description = "Security scanner for MCP bundles. Powers mpak Certified verification."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -92,6 +92,9 @@ python-version = "3.13"
|
|
|
92
92
|
|
|
93
93
|
[tool.pytest.ini_options]
|
|
94
94
|
testpaths = ["tests"]
|
|
95
|
+
markers = [
|
|
96
|
+
"e2e: end-to-end tests against real bundles from the registry (requires tests/data/)",
|
|
97
|
+
]
|
|
95
98
|
|
|
96
99
|
[dependency-groups]
|
|
97
100
|
dev = [
|
|
@@ -118,7 +118,7 @@ class AI01ValidManifest(Control):
|
|
|
118
118
|
else:
|
|
119
119
|
# Validate MTF extension structure
|
|
120
120
|
mtf_schema = get_mtf_schema()
|
|
121
|
-
mtf_errors = self._validate_schema(
|
|
121
|
+
mtf_errors = self._validate_schema(mtf_namespace, mtf_schema, "MTF")
|
|
122
122
|
for error in mtf_errors:
|
|
123
123
|
findings.append(
|
|
124
124
|
Finding(
|
|
@@ -227,8 +227,10 @@ class AI05BundleCompleteness(Control):
|
|
|
227
227
|
cleaned = arg.replace("${__dirname}/", "")
|
|
228
228
|
referenced.add(cleaned)
|
|
229
229
|
|
|
230
|
-
#
|
|
231
|
-
|
|
230
|
+
# Node.js entry point directory resolution
|
|
231
|
+
server_type = server.get("type", "") if isinstance(server, dict) else ""
|
|
232
|
+
if server_type == "node" and entry_point and bundle_dir:
|
|
233
|
+
self._add_node_entry_point_files(entry_point, bundle_dir, referenced)
|
|
232
234
|
|
|
233
235
|
return referenced
|
|
234
236
|
|
|
@@ -252,6 +254,27 @@ class AI05BundleCompleteness(Control):
|
|
|
252
254
|
if f.is_file():
|
|
253
255
|
referenced.add(str(f.relative_to(bundle_dir)).replace("\\", "/"))
|
|
254
256
|
|
|
257
|
+
def _add_node_entry_point_files(self, entry_point: str, bundle_dir: Path, referenced: set[str]) -> None:
|
|
258
|
+
"""Resolve a Node.js entry point to its sibling modules.
|
|
259
|
+
|
|
260
|
+
TypeScript compiles to a directory (build/, dist/) where the entry
|
|
261
|
+
point imports other .js files. Add all files in the entry point's
|
|
262
|
+
directory tree as referenced.
|
|
263
|
+
"""
|
|
264
|
+
entry_path = bundle_dir / entry_point
|
|
265
|
+
if not entry_path.exists():
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
# Add all files in the entry point's parent directory tree
|
|
269
|
+
entry_dir = entry_path.parent
|
|
270
|
+
if entry_dir == bundle_dir:
|
|
271
|
+
# Entry point is at root; don't add everything
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
for f in entry_dir.rglob("*"):
|
|
275
|
+
if f.is_file():
|
|
276
|
+
referenced.add(str(f.relative_to(bundle_dir)).replace("\\", "/"))
|
|
277
|
+
|
|
255
278
|
def _looks_like_file(self, arg: str) -> bool:
|
|
256
279
|
"""Check if an argument looks like a file path."""
|
|
257
280
|
# Skip flags
|
{mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/supply_chain/sc02_vuln_scan.py
RENAMED
|
@@ -12,6 +12,7 @@ Blocking logic per MTF spec:
|
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
|
+
import logging
|
|
15
16
|
import subprocess
|
|
16
17
|
import time
|
|
17
18
|
import urllib.request
|
|
@@ -21,6 +22,8 @@ from typing import Any
|
|
|
21
22
|
from mpak_scanner.controls.base import Control, ControlRegistry
|
|
22
23
|
from mpak_scanner.models import ControlResult, ControlStatus, Finding, Severity
|
|
23
24
|
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
24
27
|
# EPSS API endpoint (FIRST.org)
|
|
25
28
|
EPSS_API_URL = "https://api.first.org/data/v1/epss"
|
|
26
29
|
|
|
@@ -150,6 +153,8 @@ class SC02VulnerabilityScan(Control):
|
|
|
150
153
|
return self.error("Vulnerability scan timed out")
|
|
151
154
|
|
|
152
155
|
# Grype exits non-zero if it finds vulnerabilities, but also for errors
|
|
156
|
+
stderr_preview = result.stderr[:200] if result.stderr else ""
|
|
157
|
+
logger.info("grype exit=%d stdout=%d bytes stderr=%s", result.returncode, len(result.stdout), stderr_preview)
|
|
153
158
|
if result.returncode != 0 and "no packages discovered" not in result.stderr.lower():
|
|
154
159
|
if not result.stdout.strip():
|
|
155
160
|
return ControlResult(
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""End-to-end tests against real bundles from the mpak registry.
|
|
2
|
+
|
|
3
|
+
These tests scan real published bundles to catch false positives and
|
|
4
|
+
regressions that synthetic fixtures miss.
|
|
5
|
+
|
|
6
|
+
Setup:
|
|
7
|
+
mpak bundle pull @nimblebraininc/finnhub -o tests/data/finnhub.mcpb
|
|
8
|
+
mpak bundle pull @nimblebraininc/folk -o tests/data/folk.mcpb
|
|
9
|
+
mpak bundle pull @nimblebraininc/nationalparks -o tests/data/nationalparks.mcpb
|
|
10
|
+
|
|
11
|
+
Run:
|
|
12
|
+
uv run pytest tests/test_e2e_bundles.py -v
|
|
13
|
+
uv run pytest -m e2e -v
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from mpak_scanner import scan_bundle
|
|
21
|
+
from mpak_scanner.models import ControlStatus, Severity
|
|
22
|
+
|
|
23
|
+
DATA_DIR = Path(__file__).parent / "data"
|
|
24
|
+
|
|
25
|
+
# Bundle paths
|
|
26
|
+
FINNHUB = DATA_DIR / "finnhub.mcpb"
|
|
27
|
+
FOLK = DATA_DIR / "folk.mcpb"
|
|
28
|
+
NATIONALPARKS = DATA_DIR / "nationalparks.mcpb"
|
|
29
|
+
|
|
30
|
+
ALL_BUNDLES = [
|
|
31
|
+
pytest.param(FINNHUB, id="finnhub"),
|
|
32
|
+
pytest.param(FOLK, id="folk"),
|
|
33
|
+
pytest.param(NATIONALPARKS, id="nationalparks"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
PYTHON_BUNDLES = [
|
|
37
|
+
pytest.param(FINNHUB, id="finnhub"),
|
|
38
|
+
pytest.param(FOLK, id="folk"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
NODE_BUNDLES = [
|
|
42
|
+
pytest.param(NATIONALPARKS, id="nationalparks"),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def skip_if_missing(bundle_path: Path) -> None:
|
|
47
|
+
if not bundle_path.exists():
|
|
48
|
+
pytest.skip(f"Bundle not found: {bundle_path.name} (run: mpak bundle pull ... -o {bundle_path})")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.e2e
|
|
52
|
+
class TestBundleCompleteness:
|
|
53
|
+
"""AI-05: Real bundles should not have false-positive unexpected executables."""
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("bundle", ALL_BUNDLES)
|
|
56
|
+
def test_ai05_passes(self, bundle: Path) -> None:
|
|
57
|
+
"""AI-05 should PASS on all published bundles (no false positives)."""
|
|
58
|
+
skip_if_missing(bundle)
|
|
59
|
+
report = scan_bundle(bundle)
|
|
60
|
+
|
|
61
|
+
ai05 = report.all_controls.get("AI-05")
|
|
62
|
+
assert ai05 is not None
|
|
63
|
+
assert ai05.status == ControlStatus.PASS, f"AI-05 false positives on {bundle.name}: " + ", ".join(
|
|
64
|
+
f.title for f in ai05.findings if f.severity in {Severity.HIGH, Severity.CRITICAL}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@pytest.mark.parametrize("bundle", ALL_BUNDLES)
|
|
68
|
+
def test_no_high_or_critical_in_ai05(self, bundle: Path) -> None:
|
|
69
|
+
"""AI-05 should have zero HIGH/CRITICAL findings on published bundles."""
|
|
70
|
+
skip_if_missing(bundle)
|
|
71
|
+
report = scan_bundle(bundle)
|
|
72
|
+
|
|
73
|
+
ai05 = report.all_controls.get("AI-05")
|
|
74
|
+
assert ai05 is not None
|
|
75
|
+
blocking = [f for f in ai05.findings if f.severity in {Severity.HIGH, Severity.CRITICAL}]
|
|
76
|
+
assert blocking == [], f"Blocking findings on {bundle.name}: {[f.title for f in blocking]}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.e2e
|
|
80
|
+
class TestManifestValidation:
|
|
81
|
+
"""AI-01: Real bundles should have valid manifests."""
|
|
82
|
+
|
|
83
|
+
@pytest.mark.parametrize("bundle", ALL_BUNDLES)
|
|
84
|
+
def test_ai01_passes(self, bundle: Path) -> None:
|
|
85
|
+
skip_if_missing(bundle)
|
|
86
|
+
report = scan_bundle(bundle)
|
|
87
|
+
|
|
88
|
+
ai01 = report.all_controls.get("AI-01")
|
|
89
|
+
assert ai01 is not None
|
|
90
|
+
assert ai01.status == ControlStatus.PASS, f"AI-01 failed on {bundle.name}: {ai01.findings}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.e2e
|
|
94
|
+
class TestSafeExecution:
|
|
95
|
+
"""CQ-05: Real bundles should pass safe execution checks."""
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize("bundle", ALL_BUNDLES)
|
|
98
|
+
def test_cq05_passes(self, bundle: Path) -> None:
|
|
99
|
+
skip_if_missing(bundle)
|
|
100
|
+
report = scan_bundle(bundle)
|
|
101
|
+
|
|
102
|
+
cq05 = report.all_controls.get("CQ-05")
|
|
103
|
+
assert cq05 is not None
|
|
104
|
+
assert cq05.status == ControlStatus.PASS, f"CQ-05 failed on {bundle.name}: {cq05.findings}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.e2e
|
|
108
|
+
class TestFullScan:
|
|
109
|
+
"""Full scan results for each bundle."""
|
|
110
|
+
|
|
111
|
+
@pytest.mark.parametrize("bundle", PYTHON_BUNDLES)
|
|
112
|
+
def test_python_bundles_no_critical_findings(self, bundle: Path) -> None:
|
|
113
|
+
"""Python bundles should have no CRITICAL findings across all controls."""
|
|
114
|
+
skip_if_missing(bundle)
|
|
115
|
+
report = scan_bundle(bundle)
|
|
116
|
+
|
|
117
|
+
critical = []
|
|
118
|
+
for control_id, result in report.all_controls.items():
|
|
119
|
+
for f in result.findings:
|
|
120
|
+
if f.severity == Severity.CRITICAL:
|
|
121
|
+
critical.append(f"{control_id}: {f.title}")
|
|
122
|
+
assert critical == [], f"Critical findings on {bundle.name}: {critical}"
|
|
123
|
+
|
|
124
|
+
@pytest.mark.parametrize("bundle", NODE_BUNDLES)
|
|
125
|
+
def test_node_bundles_no_critical_findings(self, bundle: Path) -> None:
|
|
126
|
+
"""Node.js bundles should have no CRITICAL findings across all controls."""
|
|
127
|
+
skip_if_missing(bundle)
|
|
128
|
+
report = scan_bundle(bundle)
|
|
129
|
+
|
|
130
|
+
critical = []
|
|
131
|
+
for control_id, result in report.all_controls.items():
|
|
132
|
+
for f in result.findings:
|
|
133
|
+
if f.severity == Severity.CRITICAL:
|
|
134
|
+
critical.append(f"{control_id}: {f.title}")
|
|
135
|
+
assert critical == [], f"Critical findings on {bundle.name}: {critical}"
|
|
136
|
+
|
|
137
|
+
@pytest.mark.parametrize("bundle", ALL_BUNDLES)
|
|
138
|
+
def test_scan_completes_without_errors(self, bundle: Path) -> None:
|
|
139
|
+
"""Scanner should not produce ERROR status on any control."""
|
|
140
|
+
skip_if_missing(bundle)
|
|
141
|
+
report = scan_bundle(bundle)
|
|
142
|
+
|
|
143
|
+
errors = [f"{cid}: {r.findings}" for cid, r in report.all_controls.items() if r.status == ControlStatus.ERROR]
|
|
144
|
+
assert errors == [], f"Controls errored on {bundle.name}: {errors}"
|
|
@@ -862,6 +862,93 @@ class TestAI05BundleCompleteness:
|
|
|
862
862
|
assert result.status == ControlStatus.FAIL
|
|
863
863
|
assert any("backdoor.py" in f.file for f in result.findings if f.file)
|
|
864
864
|
|
|
865
|
+
def test_node_multi_file_build_passes(self, bundle_dir: Path) -> None:
|
|
866
|
+
"""Node.js entry point in build/ should treat sibling modules as referenced."""
|
|
867
|
+
manifest = {
|
|
868
|
+
"name": "@test/node-multi",
|
|
869
|
+
"version": "1.0.0",
|
|
870
|
+
"server": {
|
|
871
|
+
"type": "node",
|
|
872
|
+
"entry_point": "build/index.js",
|
|
873
|
+
"mcp_config": {
|
|
874
|
+
"command": "node",
|
|
875
|
+
"args": ["${__dirname}/build/index.js", "--stdio"],
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
}
|
|
879
|
+
(bundle_dir / "manifest.json").write_text(json.dumps(manifest))
|
|
880
|
+
(bundle_dir / "package.json").write_text('{"name": "test"}')
|
|
881
|
+
build = bundle_dir / "build"
|
|
882
|
+
build.mkdir()
|
|
883
|
+
(build / "index.js").write_text("import './config.js';")
|
|
884
|
+
(build / "config.js").write_text("export const cfg = {};")
|
|
885
|
+
(build / "schemas.js").write_text("export const schemas = {};")
|
|
886
|
+
handlers = build / "handlers"
|
|
887
|
+
handlers.mkdir()
|
|
888
|
+
(handlers / "findParks.js").write_text("export function findParks() {}")
|
|
889
|
+
(handlers / "getAlerts.js").write_text("export function getAlerts() {}")
|
|
890
|
+
|
|
891
|
+
from mpak_scanner.controls.artifact_integrity import AI05BundleCompleteness
|
|
892
|
+
|
|
893
|
+
control = AI05BundleCompleteness()
|
|
894
|
+
result = control.run(bundle_dir, manifest)
|
|
895
|
+
assert result.status == ControlStatus.PASS
|
|
896
|
+
|
|
897
|
+
def test_node_build_with_stray_script_fails(self, bundle_dir: Path) -> None:
|
|
898
|
+
"""Node.js build/ files are allowed but stray scripts at root should fail."""
|
|
899
|
+
manifest = {
|
|
900
|
+
"name": "@test/node-stray",
|
|
901
|
+
"version": "1.0.0",
|
|
902
|
+
"server": {
|
|
903
|
+
"type": "node",
|
|
904
|
+
"entry_point": "build/index.js",
|
|
905
|
+
"mcp_config": {
|
|
906
|
+
"command": "node",
|
|
907
|
+
"args": ["${__dirname}/build/index.js"],
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
}
|
|
911
|
+
(bundle_dir / "manifest.json").write_text(json.dumps(manifest))
|
|
912
|
+
build = bundle_dir / "build"
|
|
913
|
+
build.mkdir()
|
|
914
|
+
(build / "index.js").write_text("console.log('ok')")
|
|
915
|
+
# Stray script outside build/
|
|
916
|
+
(bundle_dir / "deploy.sh").write_text("#!/bin/bash\nrm -rf /")
|
|
917
|
+
|
|
918
|
+
from mpak_scanner.controls.artifact_integrity import AI05BundleCompleteness
|
|
919
|
+
|
|
920
|
+
control = AI05BundleCompleteness()
|
|
921
|
+
result = control.run(bundle_dir, manifest)
|
|
922
|
+
assert result.status == ControlStatus.FAIL
|
|
923
|
+
assert any("deploy.sh" in f.file for f in result.findings if f.file)
|
|
924
|
+
# build/index.js should NOT be flagged
|
|
925
|
+
assert not any("build/index.js" in f.file for f in result.findings if f.file)
|
|
926
|
+
|
|
927
|
+
def test_node_root_entry_point_no_over_allow(self, bundle_dir: Path) -> None:
|
|
928
|
+
"""Node.js entry point at root should not whitelist all files."""
|
|
929
|
+
manifest = {
|
|
930
|
+
"name": "@test/node-root",
|
|
931
|
+
"version": "1.0.0",
|
|
932
|
+
"server": {
|
|
933
|
+
"type": "node",
|
|
934
|
+
"entry_point": "index.js",
|
|
935
|
+
"mcp_config": {
|
|
936
|
+
"command": "node",
|
|
937
|
+
"args": ["${__dirname}/index.js"],
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
}
|
|
941
|
+
(bundle_dir / "manifest.json").write_text(json.dumps(manifest))
|
|
942
|
+
(bundle_dir / "index.js").write_text("console.log('ok')")
|
|
943
|
+
(bundle_dir / "backdoor.js").write_text("require('child_process').exec('evil')")
|
|
944
|
+
|
|
945
|
+
from mpak_scanner.controls.artifact_integrity import AI05BundleCompleteness
|
|
946
|
+
|
|
947
|
+
control = AI05BundleCompleteness()
|
|
948
|
+
result = control.run(bundle_dir, manifest)
|
|
949
|
+
assert result.status == ControlStatus.FAIL
|
|
950
|
+
assert any("backdoor.js" in f.file for f in result.findings if f.file)
|
|
951
|
+
|
|
865
952
|
|
|
866
953
|
# =============================================================================
|
|
867
954
|
# Fixture-based Integration Tests
|
mpak_scanner-0.2.2/Dockerfile
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/artifact_integrity/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/code_quality/__init__.py
RENAMED
|
File without changes
|
{mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/code_quality/cq01_secrets.py
RENAMED
|
File without changes
|
{mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/code_quality/cq02_malicious.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/supply_chain/__init__.py
RENAMED
|
File without changes
|
{mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/src/mpak_scanner/controls/supply_chain/sc01_sbom.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/invalid-manifest-bundle/manifest.json
RENAMED
|
File without changes
|
{mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/invalid-manifest-bundle/src/server.py
RENAMED
|
File without changes
|
{mpak_scanner-0.2.2 → mpak_scanner-0.2.4}/tests/fixtures/malicious-test-bundle/manifest.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|