scan-now 0.0.0-stage → 1.0.0

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.
Files changed (3) hide show
  1. package/README.md +14 -2
  2. package/bin/scan.js +131 -0
  3. package/package.json +12 -4
package/README.md CHANGED
@@ -1,3 +1,15 @@
1
- # Temporary Holding Version
1
+ # Scan
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run Scan.now checks from your terminal.
4
+
5
+ ```sh
6
+ npm install --global scan-now
7
+ scan website example.com
8
+ scan seo example.com --pages 25
9
+ scan technology example.com
10
+ scan phishing https://example.com/login
11
+ scan file download.zip
12
+ scan extension addon.zip
13
+ ```
14
+
15
+ Use `--json` for the full API response. Scan limits and account access rules are the same as on <https://scan.now>.
package/bin/scan.js ADDED
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const pkg = require('../package.json');
7
+
8
+ const base = (process.env.SCAN_NOW_URL || 'https://scan.now').replace(/\/$/, '');
9
+ const commands = new Set(['website', 'seo', 'technology', 'wordpress', 'phishing', 'file', 'extension']);
10
+
11
+ function help() {
12
+ return `Scan ${pkg.version}
13
+
14
+ Usage:
15
+ scan <url>
16
+ scan website <url> [--scope full|tls|headers|email|js]
17
+ scan seo <url> [--pages 25]
18
+ scan technology <url>
19
+ scan wordpress <url>
20
+ scan phishing <url>
21
+ scan file <path>
22
+ scan extension <path>
23
+
24
+ Options:
25
+ --json Print JSON
26
+ --pages N SEO/website page budget
27
+ --scope NAME Website scan scope`;
28
+ }
29
+
30
+ function parse(argv) {
31
+ const args = [...argv];
32
+ if (!args.length || args.includes('--help') || args.includes('-h')) return { help: true };
33
+ if (args.includes('--version') || args.includes('-v')) return { version: true };
34
+ const json = args.includes('--json');
35
+ if (json) args.splice(args.indexOf('--json'), 1);
36
+ const option = (name, fallback) => {
37
+ const index = args.indexOf(name);
38
+ if (index < 0) return fallback;
39
+ const value = args[index + 1];
40
+ if (!value) throw new Error(`${name} requires a value.`);
41
+ args.splice(index, 2);
42
+ return value;
43
+ };
44
+ const pages = Number(option('--pages', '25'));
45
+ const scope = option('--scope', 'full');
46
+ if (!Number.isInteger(pages) || pages < 1 || pages > 5000) throw new Error('--pages must be an integer from 1 to 5000.');
47
+ const command = commands.has(args[0]) ? args.shift() : 'website';
48
+ return { command, target: args.join(' ').trim(), pages, scope, json };
49
+ }
50
+
51
+ class Session {
52
+ constructor() { this.cookie = ''; this.csrf = ''; }
53
+ capture(response) {
54
+ const values = response.headers.getSetCookie?.() || [response.headers.get('set-cookie')].filter(Boolean);
55
+ if (values.length) this.cookie = values.map((value) => value.split(';', 1)[0]).join('; ');
56
+ }
57
+ async start() {
58
+ const response = await fetch(`${base}/api/csrf`, { headers: { Accept: 'application/json', 'User-Agent': `scan-now/${pkg.version}` } });
59
+ this.capture(response);
60
+ const data = await response.json().catch(() => ({}));
61
+ if (!response.ok || !data.csrf_token) throw new Error(data.error || `Scan.now session failed with HTTP ${response.status}.`);
62
+ this.csrf = data.csrf_token;
63
+ }
64
+ async post(endpoint, body, multipart = false) {
65
+ const response = await fetch(`${base}${endpoint}`, {
66
+ method: 'POST',
67
+ headers: {
68
+ 'X-CSRFToken': this.csrf,
69
+ Cookie: this.cookie,
70
+ Referer: `${base}/`,
71
+ 'User-Agent': `scan-now/${pkg.version}`,
72
+ ...(multipart ? {} : { 'Content-Type': 'application/json' }),
73
+ },
74
+ body: multipart ? body : JSON.stringify(body),
75
+ signal: AbortSignal.timeout(180_000),
76
+ });
77
+ const data = await response.json().catch(() => ({}));
78
+ if (!response.ok) throw new Error(data.error || `Scan.now returned HTTP ${response.status}.`);
79
+ return data;
80
+ }
81
+ }
82
+
83
+ function clean(data) {
84
+ const output = { ...data };
85
+ for (const key of ['html', 'next_html', 'ai_prompt']) delete output[key];
86
+ return output;
87
+ }
88
+
89
+ function summary(data) {
90
+ const lines = [];
91
+ if (data.host || data.target) lines.push(`Target: ${data.host || data.target}`);
92
+ if (data.grade) lines.push(`Grade: ${data.grade}`);
93
+ if (data.score != null) lines.push(`Score: ${data.score}`);
94
+ if (data.verdict) lines.push(`Verdict: ${typeof data.verdict === 'string' ? data.verdict : data.verdict.label || 'complete'}`);
95
+ if (data.risk != null) lines.push(`Risk: ${data.risk}`);
96
+ if (Array.isArray(data.technologies)) lines.push(`Technologies: ${data.technologies.map((item) => item.name).filter(Boolean).join(', ') || 'none detected'}`);
97
+ if (data.report_url) lines.push(`Report: ${new URL(data.report_url, base)}`);
98
+ if (data.locked) lines.push(data.message || 'Results require an account upgrade.');
99
+ return lines.join('\n') || 'Scan complete.';
100
+ }
101
+
102
+ async function main() {
103
+ const options = parse(process.argv.slice(2));
104
+ if (options.help) return process.stdout.write(`${help()}\n`);
105
+ if (options.version) return process.stdout.write(`${pkg.version}\n`);
106
+ if (!options.target) throw new Error(`${options.command} requires a URL or file path.`);
107
+ const session = new Session();
108
+ await session.start();
109
+ let data;
110
+ if (options.command === 'file' || options.command === 'extension') {
111
+ const filename = path.resolve(options.target);
112
+ if (!fs.existsSync(filename)) throw new Error(`File not found: ${options.target}`);
113
+ const form = new FormData();
114
+ form.append('file', await fs.openAsBlob(filename), path.basename(filename));
115
+ data = await session.post(`/api/scan/${options.command}`, form, true);
116
+ } else {
117
+ const payload = { url: options.target };
118
+ if (options.command === 'website') Object.assign(payload, { scope: options.scope, pages: options.pages });
119
+ if (options.command === 'seo') payload.pages = options.pages;
120
+ data = await session.post(`/api/scan/${options.command}`, payload);
121
+ }
122
+ const output = clean(data);
123
+ process.stdout.write(options.json ? `${JSON.stringify(output, null, 2)}\n` : `${summary(output)}\n`);
124
+ }
125
+
126
+ if (require.main === module) main().catch((error) => {
127
+ process.stderr.write(`scan: ${error.name === 'TimeoutError' ? 'The scan timed out.' : error.message}\n`);
128
+ process.exitCode = 1;
129
+ });
130
+
131
+ module.exports = { parse, clean };
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "name": "scan-now",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Run website, SEO, technology, phishing, file, and extension security scans from your terminal.",
5
+ "bin": { "scan": "bin/scan.js" },
6
+ "files": ["bin", "README.md"],
7
+ "scripts": { "test": "node --check bin/scan.js && node bin/scan.js --version", "prepublishOnly": "npm test" },
8
+ "engines": { "node": ">=20" },
9
+ "keywords": ["security", "scanner", "website-scan", "seo-audit", "phishing", "cli", "scan-now"],
10
+ "homepage": "https://scan.now",
11
+ "license": "UNLICENSED",
12
+ "author": "Scan.now",
13
+ "publishConfig": { "access": "public" }
14
+ }