@westbayberry/dg 1.0.10 → 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 +194 -158
- 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
|
@@ -3296,7 +3296,7 @@ var require_react_development = __commonJS({
|
|
|
3296
3296
|
}
|
|
3297
3297
|
return dispatcher.useContext(Context);
|
|
3298
3298
|
}
|
|
3299
|
-
function
|
|
3299
|
+
function useState6(initialState) {
|
|
3300
3300
|
var dispatcher = resolveDispatcher();
|
|
3301
3301
|
return dispatcher.useState(initialState);
|
|
3302
3302
|
}
|
|
@@ -3308,7 +3308,7 @@ var require_react_development = __commonJS({
|
|
|
3308
3308
|
var dispatcher = resolveDispatcher();
|
|
3309
3309
|
return dispatcher.useRef(initialValue);
|
|
3310
3310
|
}
|
|
3311
|
-
function
|
|
3311
|
+
function useEffect13(create2, deps) {
|
|
3312
3312
|
var dispatcher = resolveDispatcher();
|
|
3313
3313
|
return dispatcher.useEffect(create2, deps);
|
|
3314
3314
|
}
|
|
@@ -4091,7 +4091,7 @@ var require_react_development = __commonJS({
|
|
|
4091
4091
|
exports.useContext = useContext7;
|
|
4092
4092
|
exports.useDebugValue = useDebugValue;
|
|
4093
4093
|
exports.useDeferredValue = useDeferredValue;
|
|
4094
|
-
exports.useEffect =
|
|
4094
|
+
exports.useEffect = useEffect13;
|
|
4095
4095
|
exports.useId = useId;
|
|
4096
4096
|
exports.useImperativeHandle = useImperativeHandle;
|
|
4097
4097
|
exports.useInsertionEffect = useInsertionEffect;
|
|
@@ -4099,7 +4099,7 @@ var require_react_development = __commonJS({
|
|
|
4099
4099
|
exports.useMemo = useMemo4;
|
|
4100
4100
|
exports.useReducer = useReducer5;
|
|
4101
4101
|
exports.useRef = useRef6;
|
|
4102
|
-
exports.useState =
|
|
4102
|
+
exports.useState = useState6;
|
|
4103
4103
|
exports.useSyncExternalStore = useSyncExternalStore;
|
|
4104
4104
|
exports.useTransition = useTransition;
|
|
4105
4105
|
exports.version = ReactVersion;
|
|
@@ -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) {
|
|
@@ -40963,14 +40984,20 @@ function reducer3(_state, action) {
|
|
|
40963
40984
|
function useScan(config) {
|
|
40964
40985
|
const [state, dispatch] = (0, import_react27.useReducer)(reducer3, { phase: "discovering" });
|
|
40965
40986
|
const started = (0, import_react27.useRef)(false);
|
|
40966
|
-
const
|
|
40987
|
+
const [multiProjects, setMultiProjects] = (0, import_react27.useState)(null);
|
|
40967
40988
|
(0, import_react27.useEffect)(() => {
|
|
40968
40989
|
if (started.current) return;
|
|
40969
40990
|
started.current = true;
|
|
40991
|
+
const projects = discoverProjects(process.cwd());
|
|
40992
|
+
if (projects.length > 1) setMultiProjects(projects);
|
|
40970
40993
|
try {
|
|
40971
40994
|
const discovery = discoverChanges(process.cwd(), config);
|
|
40972
40995
|
const packages = discovery.packages.filter((p) => !config.allowlist.includes(p.name));
|
|
40973
40996
|
if (packages.length === 0) {
|
|
40997
|
+
if (discovery.packages.length === 0 && projects.length > 0) {
|
|
40998
|
+
dispatch({ type: "PROJECTS_FOUND", projects });
|
|
40999
|
+
return;
|
|
41000
|
+
}
|
|
40974
41001
|
const message = discovery.packages.length === 0 ? "No package changes detected." : "All changed packages are allowlisted.";
|
|
40975
41002
|
dispatch({ type: "DISCOVERY_EMPTY", message });
|
|
40976
41003
|
return;
|
|
@@ -40978,8 +41005,6 @@ function useScan(config) {
|
|
|
40978
41005
|
dispatch({ type: "DISCOVERY_COMPLETE", packages, skippedCount: discovery.skipped.length });
|
|
40979
41006
|
runNpmScan(packages, discovery.skipped.length, config, dispatch);
|
|
40980
41007
|
} catch {
|
|
40981
|
-
const projects = discoverProjects(process.cwd());
|
|
40982
|
-
discoveredProjects.current = projects.length > 1 ? projects : null;
|
|
40983
41008
|
if (projects.length === 0) {
|
|
40984
41009
|
dispatch({ type: "DISCOVERY_EMPTY", message: "No dependency files found." });
|
|
40985
41010
|
return;
|
|
@@ -40997,13 +41022,13 @@ function useScan(config) {
|
|
|
40997
41022
|
scanProjects(projects, config, dispatch);
|
|
40998
41023
|
}, [config]);
|
|
40999
41024
|
const restartSelection = (0, import_react27.useCallback)(() => {
|
|
41000
|
-
if (!
|
|
41001
|
-
dispatch({ type: "RESTART_SELECTION", projects:
|
|
41002
|
-
}, []);
|
|
41025
|
+
if (!multiProjects) return;
|
|
41026
|
+
dispatch({ type: "RESTART_SELECTION", projects: multiProjects });
|
|
41027
|
+
}, [multiProjects]);
|
|
41003
41028
|
return {
|
|
41004
41029
|
state,
|
|
41005
41030
|
scanSelectedProjects,
|
|
41006
|
-
restartSelection:
|
|
41031
|
+
restartSelection: multiProjects ? restartSelection : null
|
|
41007
41032
|
};
|
|
41008
41033
|
}
|
|
41009
41034
|
async function runNpmScan(packages, skippedCount, config, dispatch) {
|
|
@@ -41189,7 +41214,7 @@ function groupPackages3(packages) {
|
|
|
41189
41214
|
group.push(pkg);
|
|
41190
41215
|
map.set(fingerprint, group);
|
|
41191
41216
|
}
|
|
41192
|
-
return [...map.
|
|
41217
|
+
return [...map.entries()].map(([fingerprint, pkgs]) => ({ packages: pkgs, key: fingerprint })).sort((a, b) => b.packages[0].score - a.packages[0].score);
|
|
41193
41218
|
}
|
|
41194
41219
|
function actionBadge4(score, config) {
|
|
41195
41220
|
if (score >= config.blockThreshold)
|
|
@@ -41394,6 +41419,11 @@ var init_InteractiveResultsView = __esm({
|
|
|
41394
41419
|
if (expIdx === cursor && expLvl === "detail") return;
|
|
41395
41420
|
const newVp = adjustViewport(cursor, cursor, "detail", vpStart);
|
|
41396
41421
|
dispatchView({ type: "EXPAND", expandedIndex: cursor, expandLevel: "detail", viewport: newVp });
|
|
41422
|
+
} else if (input === "c") {
|
|
41423
|
+
if (expIdx !== null && expLvl !== null) {
|
|
41424
|
+
const newVp = adjustViewport(cursor, null, null, vpStart);
|
|
41425
|
+
dispatchView({ type: "EXPAND", expandedIndex: null, expandLevel: null, viewport: newVp });
|
|
41426
|
+
}
|
|
41397
41427
|
} else if (input === "b" && onBack) {
|
|
41398
41428
|
onBack();
|
|
41399
41429
|
} else if (input === "q") {
|
|
@@ -41519,7 +41549,10 @@ var init_InteractiveResultsView = __esm({
|
|
|
41519
41549
|
" toggle",
|
|
41520
41550
|
" ",
|
|
41521
41551
|
import_chalk10.default.cyan("e"),
|
|
41522
|
-
"
|
|
41552
|
+
" expand",
|
|
41553
|
+
" ",
|
|
41554
|
+
import_chalk10.default.cyan("c"),
|
|
41555
|
+
" collapse",
|
|
41523
41556
|
" ",
|
|
41524
41557
|
onBack && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
|
|
41525
41558
|
import_chalk10.default.cyan("b"),
|
|
@@ -41665,6 +41698,9 @@ var init_ProjectSelector = __esm({
|
|
|
41665
41698
|
import_jsx_runtime11 = __toESM(require_jsx_runtime());
|
|
41666
41699
|
ProjectSelector = ({ projects, onConfirm, onCancel }) => {
|
|
41667
41700
|
const [cursor, setCursor] = (0, import_react30.useState)(0);
|
|
41701
|
+
(0, import_react30.useEffect)(() => {
|
|
41702
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
41703
|
+
}, []);
|
|
41668
41704
|
const [selected, setSelected] = (0, import_react30.useState)(() => new Set(projects.map((_, i) => i)));
|
|
41669
41705
|
use_input_default((input, key) => {
|
|
41670
41706
|
if (key.upArrow) {
|
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"
|