install-guard 1.1.1 → 1.1.2
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 +197 -120
- package/bin/cli.js +28 -7
- package/package.json +5 -1
- package/src/analyze.js +16 -10
- package/src/checks/dependencyDiff.js +114 -0
- package/src/checks/deprecation.js +24 -0
- package/src/checks/githubVerify.js +67 -0
- package/src/checks/index.js +8 -0
- package/src/checks/license.js +35 -0
- package/src/checks/maintainers.js +30 -0
- package/src/checks/recentPublish.js +70 -0
- package/src/checks/scripts.js +45 -0
- package/src/checks/typosquat.js +77 -0
- package/src/format.js +143 -87
- package/src/index.js +2 -2
- package/src/install.js +16 -10
- package/src/scan.js +13 -11
- package/src/services/pipeline.js +105 -0
- package/src/services/scorer.js +36 -0
- package/src/utils/cache.js +44 -0
- package/src/utils/github.js +64 -0
- package/src/utils/registry.js +99 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getCached, setCache } from "./cache.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extracts owner/repo from a repository URL.
|
|
5
|
+
*/
|
|
6
|
+
function parseRepoUrl(url) {
|
|
7
|
+
if (!url) return null;
|
|
8
|
+
// Handles: git+https://github.com/owner/repo.git, https://github.com/owner/repo, etc.
|
|
9
|
+
const match = url.match(
|
|
10
|
+
/github\.com[/:]([^/]+)\/([^/.#]+)/
|
|
11
|
+
);
|
|
12
|
+
if (!match) return null;
|
|
13
|
+
return { owner: match[1], repo: match[2] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function ghFetch(path) {
|
|
17
|
+
const headers = { "User-Agent": "install-guard-cli" };
|
|
18
|
+
// Use token if available to avoid rate limits
|
|
19
|
+
if (process.env.GITHUB_TOKEN) {
|
|
20
|
+
headers.Authorization = `token ${process.env.GITHUB_TOKEN}`;
|
|
21
|
+
}
|
|
22
|
+
const res = await fetch(`https://api.github.com${path}`, { headers });
|
|
23
|
+
if (!res.ok) return null;
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Checks if a given version tag exists on GitHub.
|
|
29
|
+
* Tries both `v1.2.3` and `1.2.3` tag formats.
|
|
30
|
+
*/
|
|
31
|
+
export async function checkGitHubTag(repoUrl, version) {
|
|
32
|
+
const repo = parseRepoUrl(repoUrl);
|
|
33
|
+
if (!repo) return { hasRepo: false, tagFound: false, recentCommits: false };
|
|
34
|
+
|
|
35
|
+
const key = `gh-tag:${repo.owner}/${repo.repo}:${version}`;
|
|
36
|
+
const cached = getCached(key);
|
|
37
|
+
if (cached) return cached;
|
|
38
|
+
|
|
39
|
+
// Try v-prefixed and plain tag
|
|
40
|
+
const tags = await ghFetch(`/repos/${repo.owner}/${repo.repo}/tags?per_page=100`);
|
|
41
|
+
if (!tags) {
|
|
42
|
+
const result = { hasRepo: true, tagFound: false, recentCommits: false, error: "rate-limited or private" };
|
|
43
|
+
setCache(key, result);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const tagNames = tags.map((t) => t.name);
|
|
48
|
+
const tagFound = tagNames.includes(`v${version}`) || tagNames.includes(version);
|
|
49
|
+
|
|
50
|
+
// Check recent commits
|
|
51
|
+
const commits = await ghFetch(
|
|
52
|
+
`/repos/${repo.owner}/${repo.repo}/commits?per_page=1`
|
|
53
|
+
);
|
|
54
|
+
let recentCommits = false;
|
|
55
|
+
if (commits && commits.length > 0) {
|
|
56
|
+
const lastCommitDate = new Date(commits[0].commit?.committer?.date || 0);
|
|
57
|
+
const daysSinceCommit = (Date.now() - lastCommitDate.getTime()) / (1000 * 60 * 60 * 24);
|
|
58
|
+
recentCommits = daysSinceCommit < 90;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = { hasRepo: true, tagFound, recentCommits };
|
|
62
|
+
setCache(key, result);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { getCached, setCache } from "./cache.js";
|
|
2
|
+
|
|
3
|
+
function encodePkg(pkg) {
|
|
4
|
+
return encodeURIComponent(pkg).replace("%40", "@");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async function fetchJSON(url) {
|
|
8
|
+
const res = await fetch(url);
|
|
9
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
10
|
+
return res.json();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fetches full registry metadata for a package.
|
|
15
|
+
* Returns the raw document from registry.npmjs.org/<pkg>
|
|
16
|
+
*/
|
|
17
|
+
export async function getRegistryData(pkg) {
|
|
18
|
+
const key = `registry:${pkg}`;
|
|
19
|
+
const cached = getCached(key);
|
|
20
|
+
if (cached) return cached;
|
|
21
|
+
|
|
22
|
+
const data = await fetchJSON(`https://registry.npmjs.org/${encodePkg(pkg)}`);
|
|
23
|
+
setCache(key, data);
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fetches weekly download count.
|
|
29
|
+
*/
|
|
30
|
+
export async function getDownloads(pkg) {
|
|
31
|
+
const key = `downloads:${pkg}`;
|
|
32
|
+
const cached = getCached(key);
|
|
33
|
+
if (cached) return cached;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const data = await fetchJSON(
|
|
37
|
+
`https://api.npmjs.org/downloads/point/last-week/${encodePkg(pkg)}`
|
|
38
|
+
);
|
|
39
|
+
setCache(key, data);
|
|
40
|
+
return data;
|
|
41
|
+
} catch {
|
|
42
|
+
return { downloads: 0 };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolves version, fetches metadata + downloads, returns a normalized context
|
|
48
|
+
* that every check module can consume.
|
|
49
|
+
*/
|
|
50
|
+
export async function buildContext(pkg, requestedVersion) {
|
|
51
|
+
const registry = await getRegistryData(pkg);
|
|
52
|
+
const latest = registry["dist-tags"]?.latest;
|
|
53
|
+
if (!latest) throw new Error(`No published version found for "${pkg}"`);
|
|
54
|
+
|
|
55
|
+
const version = requestedVersion || latest;
|
|
56
|
+
const versionData = registry.versions?.[version];
|
|
57
|
+
if (!versionData) throw new Error(`Version "${version}" not found for "${pkg}"`);
|
|
58
|
+
|
|
59
|
+
const timeData = registry.time || {};
|
|
60
|
+
const allVersions = Object.keys(registry.versions || {});
|
|
61
|
+
const versionIndex = allVersions.indexOf(version);
|
|
62
|
+
const previousVersion = versionIndex > 0 ? allVersions[versionIndex - 1] : null;
|
|
63
|
+
const previousVersionData = previousVersion
|
|
64
|
+
? registry.versions[previousVersion]
|
|
65
|
+
: null;
|
|
66
|
+
|
|
67
|
+
const downloads = await getDownloads(pkg);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
name: registry.name,
|
|
71
|
+
version,
|
|
72
|
+
previousVersion,
|
|
73
|
+
description: registry.description || "",
|
|
74
|
+
downloads: downloads.downloads || 0,
|
|
75
|
+
maintainers: registry.maintainers || [],
|
|
76
|
+
license: versionData.license || registry.license || "Unknown",
|
|
77
|
+
publishedAt: timeData[version],
|
|
78
|
+
previousPublishedAt: previousVersion ? timeData[previousVersion] : null,
|
|
79
|
+
firstPublished: timeData.created,
|
|
80
|
+
repository: registry.repository?.url || versionData.repository?.url || null,
|
|
81
|
+
deprecated: versionData.deprecated || false,
|
|
82
|
+
totalVersions: allVersions.length,
|
|
83
|
+
allVersions,
|
|
84
|
+
|
|
85
|
+
// Script data
|
|
86
|
+
scripts: versionData.scripts || {},
|
|
87
|
+
|
|
88
|
+
// Dependency data
|
|
89
|
+
dependencies: versionData.dependencies || {},
|
|
90
|
+
previousDependencies: previousVersionData?.dependencies || {},
|
|
91
|
+
|
|
92
|
+
// Maintainer history — registry only exposes current maintainers
|
|
93
|
+
currentMaintainers: registry.maintainers || [],
|
|
94
|
+
|
|
95
|
+
// Raw registry for advanced checks
|
|
96
|
+
_registry: registry,
|
|
97
|
+
_versionData: versionData,
|
|
98
|
+
};
|
|
99
|
+
}
|