@westbayberry/dg 1.0.13 → 1.0.14
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.
- package/README.md +88 -17
- package/dist/index.mjs +170 -145
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @westbayberry/dg
|
|
2
2
|
|
|
3
|
-
Supply chain security scanner for npm and Python dependencies.
|
|
3
|
+
Supply chain security scanner for npm and Python dependencies. Scans lockfile changes against 26+ detectors to catch malicious packages, typosquatting, dependency confusion, credential theft, and obfuscated code before they reach production.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,6 +8,8 @@ Supply chain security scanner for npm and Python dependencies. Detects malicious
|
|
|
8
8
|
npm install -g @westbayberry/dg
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Requires Node.js 18+.
|
|
12
|
+
|
|
11
13
|
## Quick Start
|
|
12
14
|
|
|
13
15
|
```bash
|
|
@@ -15,48 +17,93 @@ dg login
|
|
|
15
17
|
dg scan
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
The CLI
|
|
20
|
+
The CLI walks your directory tree and finds npm lockfiles (`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`) and Python dependency files (`requirements.txt`, `Pipfile.lock`, `poetry.lock`). If multiple projects are found, an interactive selector lets you pick which ones to scan.
|
|
21
|
+
|
|
22
|
+
Only changed packages are scanned by default — `dg` diffs your lockfile against the git merge-base with `main` to find what's new or updated.
|
|
19
23
|
|
|
20
24
|
## Commands
|
|
21
25
|
|
|
22
26
|
```
|
|
23
|
-
dg scan [options] Scan dependencies
|
|
24
|
-
dg npm install <pkg> Scan packages before installing
|
|
27
|
+
dg scan [options] Scan dependencies for supply chain threats
|
|
28
|
+
dg npm install <pkg> Scan packages before installing them
|
|
25
29
|
dg login Authenticate with your WestBayBerry account
|
|
26
30
|
dg logout Remove saved credentials
|
|
27
|
-
dg hook install Install git pre-commit hook
|
|
31
|
+
dg hook install Install git pre-commit hook
|
|
28
32
|
dg hook uninstall Remove the pre-commit hook
|
|
29
33
|
dg update Check for and install the latest version
|
|
30
34
|
dg wrap Show instructions to alias npm to dg
|
|
31
35
|
```
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
## What It Detects
|
|
38
|
+
|
|
39
|
+
Each package is analyzed by 26+ detectors covering:
|
|
40
|
+
|
|
41
|
+
| Category | Examples |
|
|
42
|
+
|----------|----------|
|
|
43
|
+
| **Code execution** | `child_process` spawning, `eval`/`Function` calls, shell command injection |
|
|
44
|
+
| **Network exfiltration** | HTTP/WebSocket/DNS/gRPC calls, URL obfuscation, data exfil patterns |
|
|
45
|
+
| **Credential theft** | Reading SSH keys, browser tokens, cloud credentials, `.npmrc`/`.pypirc` |
|
|
46
|
+
| **Install scripts** | Suspicious `preinstall`/`postinstall` hooks, download-and-execute chains |
|
|
47
|
+
| **Obfuscation** | Hex/unicode encoding, string reconstruction, phantom eval, minified payloads |
|
|
48
|
+
| **Supply chain** | Typosquatting, dependency confusion, version squatting, borrowed repo URLs |
|
|
49
|
+
| **Persistence** | Writing to shell configs, cron jobs, systemd units, SSH `authorized_keys` |
|
|
50
|
+
| **Behavioral** | Time-gated payloads, purpose mismatch, runtime evasion, binary addons |
|
|
51
|
+
| **Reputation** | Missing/fake GitHub repos, ghost packages, low download counts |
|
|
52
|
+
|
|
53
|
+
Findings include severity (1–5), confidence (0–1), and code evidence with file paths and line numbers.
|
|
54
|
+
|
|
55
|
+
## Scan Options
|
|
34
56
|
|
|
35
57
|
| Flag | Default | Description |
|
|
36
58
|
|------|---------|-------------|
|
|
37
59
|
| `--mode <mode>` | `warn` | `block` / `warn` / `off` |
|
|
38
|
-
| `--block-threshold <n>` | `70` | Score threshold for blocking |
|
|
39
|
-
| `--warn-threshold <n>` | `60` | Score threshold for warnings |
|
|
60
|
+
| `--block-threshold <n>` | `70` | Score threshold for blocking (0–100) |
|
|
61
|
+
| `--warn-threshold <n>` | `60` | Score threshold for warnings (0–100) |
|
|
40
62
|
| `--max-packages <n>` | `200` | Max packages per scan |
|
|
41
63
|
| `--allowlist <pkgs>` | | Comma-separated packages to skip |
|
|
42
|
-
| `--json` | | Output JSON for CI parsing |
|
|
64
|
+
| `--json` | | Output raw JSON (for CI parsing) |
|
|
43
65
|
| `--scan-all` | | Scan all packages, not just changed |
|
|
44
66
|
| `--base-lockfile <path>` | | Explicit base lockfile for diff |
|
|
45
67
|
| `--workspace <dir>` | | Scan a specific workspace subdirectory |
|
|
46
|
-
| `--debug` | | Show
|
|
68
|
+
| `--debug` | | Show discovery, batching, and timing info |
|
|
47
69
|
|
|
48
|
-
|
|
70
|
+
## Exit Codes
|
|
49
71
|
|
|
50
72
|
| Code | Meaning | CI Action |
|
|
51
73
|
|------|---------|-----------|
|
|
52
74
|
| `0` | Pass | Continue |
|
|
53
75
|
| `1` | Warning | Advisory — review recommended |
|
|
54
76
|
| `2` | Block | Fail the pipeline |
|
|
55
|
-
| `3` | Error | Internal error |
|
|
77
|
+
| `3` | Error | Internal error (auth, network, etc.) |
|
|
56
78
|
|
|
57
|
-
##
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
Settings can come from CLI flags, environment variables, or a `.dgrc.json` config file (searched in the current directory, then `~/`). CLI flags take highest precedence.
|
|
82
|
+
|
|
83
|
+
### `.dgrc.json`
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"apiKey": "dg_...",
|
|
88
|
+
"mode": "block",
|
|
89
|
+
"blockThreshold": 70,
|
|
90
|
+
"warnThreshold": 60,
|
|
91
|
+
"maxPackages": 200,
|
|
92
|
+
"allowlist": ["known-safe-pkg"]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Environment Variables
|
|
97
|
+
|
|
98
|
+
| Variable | Description |
|
|
99
|
+
|----------|-------------|
|
|
100
|
+
| `DG_API_URL` | API base URL |
|
|
101
|
+
| `DG_MODE` | `block` / `warn` / `off` |
|
|
102
|
+
| `DG_ALLOWLIST` | Comma-separated allowlist |
|
|
103
|
+
| `DG_DEBUG` | Set to `1` for diagnostic output |
|
|
104
|
+
| `DG_WORKSPACE` | Workspace subdirectory |
|
|
58
105
|
|
|
59
|
-
|
|
106
|
+
## CI Setup
|
|
60
107
|
|
|
61
108
|
### GitHub Actions
|
|
62
109
|
|
|
@@ -74,6 +121,18 @@ npx @westbayberry/dg login
|
|
|
74
121
|
npx @westbayberry/dg scan --mode block --json
|
|
75
122
|
```
|
|
76
123
|
|
|
124
|
+
The `--json` flag outputs machine-readable results. Exit code `2` signals a blocked scan — wire it into your pipeline to fail the build.
|
|
125
|
+
|
|
126
|
+
### Monorepo / Workspace
|
|
127
|
+
|
|
128
|
+
Scan a specific workspace:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
dg scan --workspace packages/api
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Or let `dg` discover all projects and pick interactively.
|
|
135
|
+
|
|
77
136
|
## Git Hook
|
|
78
137
|
|
|
79
138
|
Block commits that introduce risky dependencies:
|
|
@@ -82,22 +141,34 @@ Block commits that introduce risky dependencies:
|
|
|
82
141
|
dg hook install
|
|
83
142
|
```
|
|
84
143
|
|
|
85
|
-
This
|
|
144
|
+
This adds a pre-commit hook that runs `dg scan --mode block` whenever a lockfile is staged. If any package scores above the block threshold, the commit is rejected. Remove it with `dg hook uninstall`.
|
|
86
145
|
|
|
87
146
|
## npm Wrapper
|
|
88
147
|
|
|
89
|
-
Scan packages before
|
|
148
|
+
Scan packages before they're installed:
|
|
90
149
|
|
|
91
150
|
```bash
|
|
92
151
|
dg npm install express lodash
|
|
93
152
|
```
|
|
94
153
|
|
|
95
|
-
|
|
154
|
+
Packages are resolved and scanned through the API. If a package is blocked, you'll get a confirmation prompt — press `y` to install anyway, or use `--dg-force` to skip the prompt.
|
|
155
|
+
|
|
156
|
+
To make this the default for all `npm install` commands:
|
|
96
157
|
|
|
97
158
|
```bash
|
|
98
159
|
echo 'alias npm="dg npm"' >> ~/.zshrc
|
|
99
160
|
```
|
|
100
161
|
|
|
162
|
+
## Python Support
|
|
163
|
+
|
|
164
|
+
Python projects are detected alongside npm. The scanner reads:
|
|
165
|
+
|
|
166
|
+
- `requirements.txt` — `name==version` pins
|
|
167
|
+
- `Pipfile.lock` — default and develop sections
|
|
168
|
+
- `poetry.lock` — `[[package]]` entries
|
|
169
|
+
|
|
170
|
+
Python packages are analyzed through the same detection engine against the PyPI registry.
|
|
171
|
+
|
|
101
172
|
## Links
|
|
102
173
|
|
|
103
174
|
- [Dashboard](https://westbayberry.com/dashboard)
|
package/dist/index.mjs
CHANGED
|
@@ -39587,6 +39587,144 @@ var init_lockfile = __esm({
|
|
|
39587
39587
|
}
|
|
39588
39588
|
});
|
|
39589
39589
|
|
|
39590
|
+
// src/discover.ts
|
|
39591
|
+
var discover_exports = {};
|
|
39592
|
+
__export(discover_exports, {
|
|
39593
|
+
discoverProjects: () => discoverProjects
|
|
39594
|
+
});
|
|
39595
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync, statSync } from "node:fs";
|
|
39596
|
+
import { join as join6, relative, basename } from "node:path";
|
|
39597
|
+
function discoverProjects(root) {
|
|
39598
|
+
const projects = [];
|
|
39599
|
+
walk(root, root, 0, projects);
|
|
39600
|
+
return projects.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
39601
|
+
}
|
|
39602
|
+
function walk(dir, root, depth, out) {
|
|
39603
|
+
if (depth > MAX_DEPTH) return;
|
|
39604
|
+
for (const lockfile of NPM_LOCKFILES) {
|
|
39605
|
+
const lockPath = join6(dir, lockfile);
|
|
39606
|
+
if (existsSync6(lockPath)) {
|
|
39607
|
+
const count = countNpmPackages(lockPath);
|
|
39608
|
+
if (count > 0) {
|
|
39609
|
+
out.push({
|
|
39610
|
+
path: dir,
|
|
39611
|
+
relativePath: relative(root, dir) || ".",
|
|
39612
|
+
ecosystem: "npm",
|
|
39613
|
+
depFile: lockfile,
|
|
39614
|
+
packageCount: count
|
|
39615
|
+
});
|
|
39616
|
+
}
|
|
39617
|
+
break;
|
|
39618
|
+
}
|
|
39619
|
+
}
|
|
39620
|
+
for (const depFile of PYTHON_DEPFILES) {
|
|
39621
|
+
const depPath = join6(dir, depFile);
|
|
39622
|
+
if (existsSync6(depPath)) {
|
|
39623
|
+
const count = countPythonPackages(depPath, depFile);
|
|
39624
|
+
if (count > 0) {
|
|
39625
|
+
out.push({
|
|
39626
|
+
path: dir,
|
|
39627
|
+
relativePath: relative(root, dir) || ".",
|
|
39628
|
+
ecosystem: "pypi",
|
|
39629
|
+
depFile,
|
|
39630
|
+
packageCount: count
|
|
39631
|
+
});
|
|
39632
|
+
}
|
|
39633
|
+
break;
|
|
39634
|
+
}
|
|
39635
|
+
}
|
|
39636
|
+
let entries;
|
|
39637
|
+
try {
|
|
39638
|
+
entries = readdirSync(dir);
|
|
39639
|
+
} catch {
|
|
39640
|
+
return;
|
|
39641
|
+
}
|
|
39642
|
+
for (const entry of entries) {
|
|
39643
|
+
if (SKIP_DIRS.has(entry) || entry.startsWith(".")) continue;
|
|
39644
|
+
const full = join6(dir, entry);
|
|
39645
|
+
try {
|
|
39646
|
+
if (statSync(full).isDirectory()) {
|
|
39647
|
+
walk(full, root, depth + 1, out);
|
|
39648
|
+
}
|
|
39649
|
+
} catch {
|
|
39650
|
+
}
|
|
39651
|
+
}
|
|
39652
|
+
}
|
|
39653
|
+
function countNpmPackages(lockPath) {
|
|
39654
|
+
try {
|
|
39655
|
+
const name = basename(lockPath);
|
|
39656
|
+
const content = readFileSync7(lockPath, "utf-8");
|
|
39657
|
+
if (name === "yarn.lock") {
|
|
39658
|
+
return (content.match(/^\S.*:$/gm) || []).length;
|
|
39659
|
+
}
|
|
39660
|
+
if (name === "pnpm-lock.yaml") {
|
|
39661
|
+
return (content.match(/^\s{2}'?[/@]/gm) || []).length || estimateLines(content, 200);
|
|
39662
|
+
}
|
|
39663
|
+
const parsed = JSON.parse(content);
|
|
39664
|
+
if (parsed.packages) {
|
|
39665
|
+
return Object.keys(parsed.packages).filter((k) => k !== "").length;
|
|
39666
|
+
}
|
|
39667
|
+
if (parsed.dependencies) {
|
|
39668
|
+
return Object.keys(parsed.dependencies).length;
|
|
39669
|
+
}
|
|
39670
|
+
return 0;
|
|
39671
|
+
} catch {
|
|
39672
|
+
return 0;
|
|
39673
|
+
}
|
|
39674
|
+
}
|
|
39675
|
+
function countPythonPackages(depPath, depFile) {
|
|
39676
|
+
try {
|
|
39677
|
+
const content = readFileSync7(depPath, "utf-8");
|
|
39678
|
+
if (depFile === "Pipfile.lock") {
|
|
39679
|
+
const parsed = JSON.parse(content);
|
|
39680
|
+
const defaultCount = Object.keys(parsed.default || {}).length;
|
|
39681
|
+
const devCount = Object.keys(parsed.develop || {}).length;
|
|
39682
|
+
return defaultCount + devCount;
|
|
39683
|
+
}
|
|
39684
|
+
if (depFile === "poetry.lock") {
|
|
39685
|
+
return (content.match(/^\[\[package\]\]/gm) || []).length;
|
|
39686
|
+
}
|
|
39687
|
+
return content.split("\n").filter(
|
|
39688
|
+
(line) => line.trim() && !line.trim().startsWith("#") && !line.trim().startsWith("-")
|
|
39689
|
+
).length;
|
|
39690
|
+
} catch {
|
|
39691
|
+
return 0;
|
|
39692
|
+
}
|
|
39693
|
+
}
|
|
39694
|
+
function estimateLines(content, fallback) {
|
|
39695
|
+
const lines = content.split("\n").length;
|
|
39696
|
+
return lines > 20 ? Math.floor(lines / 4) : fallback;
|
|
39697
|
+
}
|
|
39698
|
+
var SKIP_DIRS, NPM_LOCKFILES, PYTHON_DEPFILES, MAX_DEPTH;
|
|
39699
|
+
var init_discover = __esm({
|
|
39700
|
+
"src/discover.ts"() {
|
|
39701
|
+
"use strict";
|
|
39702
|
+
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
39703
|
+
"node_modules",
|
|
39704
|
+
".venv",
|
|
39705
|
+
"venv",
|
|
39706
|
+
"__pycache__",
|
|
39707
|
+
".git",
|
|
39708
|
+
".tox",
|
|
39709
|
+
".eggs",
|
|
39710
|
+
"dist",
|
|
39711
|
+
"build",
|
|
39712
|
+
".next",
|
|
39713
|
+
".nuxt",
|
|
39714
|
+
"coverage",
|
|
39715
|
+
".cache",
|
|
39716
|
+
".pytest_cache",
|
|
39717
|
+
".mypy_cache",
|
|
39718
|
+
"validation-results",
|
|
39719
|
+
"test-fixtures",
|
|
39720
|
+
"fixtures"
|
|
39721
|
+
]);
|
|
39722
|
+
NPM_LOCKFILES = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "npm-shrinkwrap.json"];
|
|
39723
|
+
PYTHON_DEPFILES = ["requirements.txt", "Pipfile.lock", "poetry.lock"];
|
|
39724
|
+
MAX_DEPTH = 8;
|
|
39725
|
+
}
|
|
39726
|
+
});
|
|
39727
|
+
|
|
39590
39728
|
// src/static-output.ts
|
|
39591
39729
|
var static_output_exports = {};
|
|
39592
39730
|
__export(static_output_exports, {
|
|
@@ -39750,7 +39888,24 @@ async function runStatic(config) {
|
|
|
39750
39888
|
const discovery = discoverChanges(process.cwd(), config);
|
|
39751
39889
|
dbg(`discovery method: ${discovery.method}`);
|
|
39752
39890
|
if (discovery.packages.length === 0) {
|
|
39753
|
-
|
|
39891
|
+
const { discoverProjects: discoverProjects2 } = await Promise.resolve().then(() => (init_discover(), discover_exports));
|
|
39892
|
+
const subProjects = discoverProjects2(process.cwd());
|
|
39893
|
+
if (subProjects.length > 0) {
|
|
39894
|
+
process.stderr.write(import_chalk4.default.yellow(" No packages found in current directory, but found projects in subdirectories:\n\n"));
|
|
39895
|
+
for (const proj of subProjects) {
|
|
39896
|
+
const pkgLabel = proj.packageCount === 1 ? "package" : "packages";
|
|
39897
|
+
process.stderr.write(
|
|
39898
|
+
import_chalk4.default.white(` ${proj.ecosystem} `) + import_chalk4.default.cyan(proj.relativePath) + import_chalk4.default.dim(` (${proj.packageCount} ${pkgLabel})
|
|
39899
|
+
`)
|
|
39900
|
+
);
|
|
39901
|
+
}
|
|
39902
|
+
process.stderr.write(import_chalk4.default.dim("\n Use --workspace to scan a specific project:\n"));
|
|
39903
|
+
process.stderr.write(import_chalk4.default.dim(` dg scan --workspace ${subProjects[0].relativePath}${config.scanAll ? " --scan-all" : ""}
|
|
39904
|
+
|
|
39905
|
+
`));
|
|
39906
|
+
} else {
|
|
39907
|
+
process.stderr.write(import_chalk4.default.dim(" No package changes detected.\n"));
|
|
39908
|
+
}
|
|
39754
39909
|
process.exit(0);
|
|
39755
39910
|
}
|
|
39756
39911
|
const packages = discovery.packages.filter(
|
|
@@ -40037,14 +40192,14 @@ __export(hook_exports, {
|
|
|
40037
40192
|
});
|
|
40038
40193
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
40039
40194
|
import {
|
|
40040
|
-
existsSync as
|
|
40041
|
-
readFileSync as
|
|
40195
|
+
existsSync as existsSync7,
|
|
40196
|
+
readFileSync as readFileSync8,
|
|
40042
40197
|
writeFileSync as writeFileSync3,
|
|
40043
40198
|
mkdirSync,
|
|
40044
40199
|
chmodSync as chmodSync2,
|
|
40045
40200
|
unlinkSync as unlinkSync2
|
|
40046
40201
|
} from "node:fs";
|
|
40047
|
-
import { join as
|
|
40202
|
+
import { join as join7 } from "node:path";
|
|
40048
40203
|
function findGitDir() {
|
|
40049
40204
|
try {
|
|
40050
40205
|
return execFileSync2("git", ["rev-parse", "--git-dir"], {
|
|
@@ -40057,11 +40212,11 @@ function findGitDir() {
|
|
|
40057
40212
|
}
|
|
40058
40213
|
function installHook() {
|
|
40059
40214
|
const gitDir = findGitDir();
|
|
40060
|
-
const hooksDir =
|
|
40061
|
-
const hookPath =
|
|
40215
|
+
const hooksDir = join7(gitDir, "hooks");
|
|
40216
|
+
const hookPath = join7(hooksDir, "pre-commit");
|
|
40062
40217
|
mkdirSync(hooksDir, { recursive: true });
|
|
40063
|
-
if (
|
|
40064
|
-
const existing =
|
|
40218
|
+
if (existsSync7(hookPath)) {
|
|
40219
|
+
const existing = readFileSync8(hookPath, "utf-8");
|
|
40065
40220
|
if (existing.includes(HOOK_MARKER)) {
|
|
40066
40221
|
process.stderr.write(" Hook already installed.\n");
|
|
40067
40222
|
return;
|
|
@@ -40081,12 +40236,12 @@ function installHook() {
|
|
|
40081
40236
|
}
|
|
40082
40237
|
function uninstallHook() {
|
|
40083
40238
|
const gitDir = findGitDir();
|
|
40084
|
-
const hookPath =
|
|
40085
|
-
if (!
|
|
40239
|
+
const hookPath = join7(gitDir, "hooks", "pre-commit");
|
|
40240
|
+
if (!existsSync7(hookPath)) {
|
|
40086
40241
|
process.stderr.write(" No hook to remove.\n");
|
|
40087
40242
|
return;
|
|
40088
40243
|
}
|
|
40089
|
-
const content =
|
|
40244
|
+
const content = readFileSync8(hookPath, "utf-8");
|
|
40090
40245
|
if (!content.includes(HOOK_MARKER)) {
|
|
40091
40246
|
process.stderr.write(
|
|
40092
40247
|
" No Dependency Guardian hook found in pre-commit.\n"
|
|
@@ -40807,140 +40962,6 @@ var init_NpmWrapperApp = __esm({
|
|
|
40807
40962
|
}
|
|
40808
40963
|
});
|
|
40809
40964
|
|
|
40810
|
-
// src/discover.ts
|
|
40811
|
-
import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync, statSync } from "node:fs";
|
|
40812
|
-
import { join as join7, relative, basename } from "node:path";
|
|
40813
|
-
function discoverProjects(root) {
|
|
40814
|
-
const projects = [];
|
|
40815
|
-
walk(root, root, 0, projects);
|
|
40816
|
-
return projects.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
40817
|
-
}
|
|
40818
|
-
function walk(dir, root, depth, out) {
|
|
40819
|
-
if (depth > MAX_DEPTH) return;
|
|
40820
|
-
for (const lockfile of NPM_LOCKFILES) {
|
|
40821
|
-
const lockPath = join7(dir, lockfile);
|
|
40822
|
-
if (existsSync7(lockPath)) {
|
|
40823
|
-
const count = countNpmPackages(lockPath);
|
|
40824
|
-
if (count > 0) {
|
|
40825
|
-
out.push({
|
|
40826
|
-
path: dir,
|
|
40827
|
-
relativePath: relative(root, dir) || ".",
|
|
40828
|
-
ecosystem: "npm",
|
|
40829
|
-
depFile: lockfile,
|
|
40830
|
-
packageCount: count
|
|
40831
|
-
});
|
|
40832
|
-
}
|
|
40833
|
-
break;
|
|
40834
|
-
}
|
|
40835
|
-
}
|
|
40836
|
-
for (const depFile of PYTHON_DEPFILES) {
|
|
40837
|
-
const depPath = join7(dir, depFile);
|
|
40838
|
-
if (existsSync7(depPath)) {
|
|
40839
|
-
const count = countPythonPackages(depPath, depFile);
|
|
40840
|
-
if (count > 0) {
|
|
40841
|
-
out.push({
|
|
40842
|
-
path: dir,
|
|
40843
|
-
relativePath: relative(root, dir) || ".",
|
|
40844
|
-
ecosystem: "pypi",
|
|
40845
|
-
depFile,
|
|
40846
|
-
packageCount: count
|
|
40847
|
-
});
|
|
40848
|
-
}
|
|
40849
|
-
break;
|
|
40850
|
-
}
|
|
40851
|
-
}
|
|
40852
|
-
let entries;
|
|
40853
|
-
try {
|
|
40854
|
-
entries = readdirSync(dir);
|
|
40855
|
-
} catch {
|
|
40856
|
-
return;
|
|
40857
|
-
}
|
|
40858
|
-
for (const entry of entries) {
|
|
40859
|
-
if (SKIP_DIRS.has(entry) || entry.startsWith(".")) continue;
|
|
40860
|
-
const full = join7(dir, entry);
|
|
40861
|
-
try {
|
|
40862
|
-
if (statSync(full).isDirectory()) {
|
|
40863
|
-
walk(full, root, depth + 1, out);
|
|
40864
|
-
}
|
|
40865
|
-
} catch {
|
|
40866
|
-
}
|
|
40867
|
-
}
|
|
40868
|
-
}
|
|
40869
|
-
function countNpmPackages(lockPath) {
|
|
40870
|
-
try {
|
|
40871
|
-
const name = basename(lockPath);
|
|
40872
|
-
const content = readFileSync8(lockPath, "utf-8");
|
|
40873
|
-
if (name === "yarn.lock") {
|
|
40874
|
-
return (content.match(/^\S.*:$/gm) || []).length;
|
|
40875
|
-
}
|
|
40876
|
-
if (name === "pnpm-lock.yaml") {
|
|
40877
|
-
return (content.match(/^\s{2}'?[/@]/gm) || []).length || estimateLines(content, 200);
|
|
40878
|
-
}
|
|
40879
|
-
const parsed = JSON.parse(content);
|
|
40880
|
-
if (parsed.packages) {
|
|
40881
|
-
return Object.keys(parsed.packages).filter((k) => k !== "").length;
|
|
40882
|
-
}
|
|
40883
|
-
if (parsed.dependencies) {
|
|
40884
|
-
return Object.keys(parsed.dependencies).length;
|
|
40885
|
-
}
|
|
40886
|
-
return 0;
|
|
40887
|
-
} catch {
|
|
40888
|
-
return 0;
|
|
40889
|
-
}
|
|
40890
|
-
}
|
|
40891
|
-
function countPythonPackages(depPath, depFile) {
|
|
40892
|
-
try {
|
|
40893
|
-
const content = readFileSync8(depPath, "utf-8");
|
|
40894
|
-
if (depFile === "Pipfile.lock") {
|
|
40895
|
-
const parsed = JSON.parse(content);
|
|
40896
|
-
const defaultCount = Object.keys(parsed.default || {}).length;
|
|
40897
|
-
const devCount = Object.keys(parsed.develop || {}).length;
|
|
40898
|
-
return defaultCount + devCount;
|
|
40899
|
-
}
|
|
40900
|
-
if (depFile === "poetry.lock") {
|
|
40901
|
-
return (content.match(/^\[\[package\]\]/gm) || []).length;
|
|
40902
|
-
}
|
|
40903
|
-
return content.split("\n").filter(
|
|
40904
|
-
(line) => line.trim() && !line.trim().startsWith("#") && !line.trim().startsWith("-")
|
|
40905
|
-
).length;
|
|
40906
|
-
} catch {
|
|
40907
|
-
return 0;
|
|
40908
|
-
}
|
|
40909
|
-
}
|
|
40910
|
-
function estimateLines(content, fallback) {
|
|
40911
|
-
const lines = content.split("\n").length;
|
|
40912
|
-
return lines > 20 ? Math.floor(lines / 4) : fallback;
|
|
40913
|
-
}
|
|
40914
|
-
var SKIP_DIRS, NPM_LOCKFILES, PYTHON_DEPFILES, MAX_DEPTH;
|
|
40915
|
-
var init_discover = __esm({
|
|
40916
|
-
"src/discover.ts"() {
|
|
40917
|
-
"use strict";
|
|
40918
|
-
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
40919
|
-
"node_modules",
|
|
40920
|
-
".venv",
|
|
40921
|
-
"venv",
|
|
40922
|
-
"__pycache__",
|
|
40923
|
-
".git",
|
|
40924
|
-
".tox",
|
|
40925
|
-
".eggs",
|
|
40926
|
-
"dist",
|
|
40927
|
-
"build",
|
|
40928
|
-
".next",
|
|
40929
|
-
".nuxt",
|
|
40930
|
-
"coverage",
|
|
40931
|
-
".cache",
|
|
40932
|
-
".pytest_cache",
|
|
40933
|
-
".mypy_cache",
|
|
40934
|
-
"validation-results",
|
|
40935
|
-
"test-fixtures",
|
|
40936
|
-
"fixtures"
|
|
40937
|
-
]);
|
|
40938
|
-
NPM_LOCKFILES = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "npm-shrinkwrap.json"];
|
|
40939
|
-
PYTHON_DEPFILES = ["requirements.txt", "Pipfile.lock", "poetry.lock"];
|
|
40940
|
-
MAX_DEPTH = 8;
|
|
40941
|
-
}
|
|
40942
|
-
});
|
|
40943
|
-
|
|
40944
40965
|
// src/ui/hooks/useScan.ts
|
|
40945
40966
|
function reducer3(_state, action) {
|
|
40946
40967
|
switch (action.type) {
|
|
@@ -40973,6 +40994,10 @@ function useScan(config) {
|
|
|
40973
40994
|
const discovery = discoverChanges(process.cwd(), config);
|
|
40974
40995
|
const packages = discovery.packages.filter((p) => !config.allowlist.includes(p.name));
|
|
40975
40996
|
if (packages.length === 0) {
|
|
40997
|
+
if (discovery.packages.length === 0 && projects.length > 0) {
|
|
40998
|
+
dispatch({ type: "PROJECTS_FOUND", projects });
|
|
40999
|
+
return;
|
|
41000
|
+
}
|
|
40976
41001
|
const message = discovery.packages.length === 0 ? "No package changes detected." : "All changed packages are allowlisted.";
|
|
40977
41002
|
dispatch({ type: "DISCOVERY_EMPTY", message });
|
|
40978
41003
|
return;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@westbayberry/dg",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Supply chain security scanner
|
|
3
|
+
"version": "1.0.14",
|
|
4
|
+
"description": "Supply chain security scanner for npm and Python dependencies — detects malicious packages, typosquatting, dependency confusion, and 26+ attack patterns",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dependency-guardian": "dist/index.mjs",
|
|
7
7
|
"dg": "dist/index.mjs"
|
|
@@ -15,10 +15,14 @@
|
|
|
15
15
|
"keywords": [
|
|
16
16
|
"security",
|
|
17
17
|
"npm",
|
|
18
|
+
"pypi",
|
|
18
19
|
"supply-chain",
|
|
19
20
|
"scanner",
|
|
20
21
|
"cli",
|
|
21
|
-
"dependencies"
|
|
22
|
+
"dependencies",
|
|
23
|
+
"malware",
|
|
24
|
+
"typosquatting",
|
|
25
|
+
"dependency-confusion"
|
|
22
26
|
],
|
|
23
27
|
"publishConfig": {
|
|
24
28
|
"access": "public"
|