cyberhub-pracenv 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yoitzmochi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # CyberHub Practice Environment (`cyberhub-pracenv`)
2
+
3
+ A beginner-friendly, **fully terminal-based** cybersecurity (CTF) practice
4
+ platform in the ICOA terminal style. Everything happens inside one interactive
5
+ shell: browse challenges, read prompts, download attached files, and submit
6
+ flags — no web browser, no separate commands.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install -g cyberhub-pracenv@latest
12
+ ```
13
+
14
+ Or install from a local clone:
15
+
16
+ ```bash
17
+ git clone <this-repo> cyberhub-pracenv
18
+ cd cyberhub-pracenv
19
+ npm install
20
+ npm install -g .
21
+ ```
22
+
23
+ ## Run
24
+
25
+ ```bash
26
+ cyberhub-pracenv # or the short alias: chpe
27
+ ```
28
+
29
+ You'll see a welcome screen — **press Enter** to drop into the platform shell.
30
+ Type `help` at any time to see every command.
31
+
32
+ ## What you can do
33
+
34
+ | Command | Description |
35
+ | --- | --- |
36
+ | `challenges` (`ls`) | List all available challenges |
37
+ | `open <id>` | View a challenge: prompt, points, attached files |
38
+ | `files <id>` | List files attached to a challenge |
39
+ | `cat <id> <file>` | Print an attached file's contents |
40
+ | `get <id> <file>` | Copy an attached file into your launch directory |
41
+ | `submit <id> <flag>` | Submit a flag to solve a challenge |
42
+ | `stats` (`whoami`) | Your full user breakdown (points, rank, progress) |
43
+ | `admin` | Unlock admin mode (asks for the admin password) |
44
+ | `help` | Show all commands |
45
+ | `exit` (`quit`) | Leave the platform |
46
+
47
+ ### Admin (admin mode only)
48
+
49
+ | Command | Description |
50
+ | --- | --- |
51
+ | `upload` | Wizard that creates a new challenge `.yml` |
52
+ | `addfile <id> <path>` | Attach a file from disk to an existing challenge |
53
+ | `rmchallenge <id>` | Delete a challenge and its files |
54
+ | `passwd` | Change the admin password |
55
+ | `logout` | Leave admin mode |
56
+
57
+ The **default admin password is `admin`** — change it with `passwd` after your
58
+ first login.
59
+
60
+ ## Challenge format
61
+
62
+ Challenges are plain `.yml` files (created by the `upload` wizard or by hand):
63
+
64
+ ```yaml
65
+ id: web-101
66
+ title: Cookie Monster
67
+ category: web
68
+ difficulty: easy
69
+ points: 100
70
+ description: |
71
+ Multi-line prompt describing the challenge.
72
+ flag: CHPE{example_flag}
73
+ files:
74
+ - cookies.txt
75
+ ```
76
+
77
+ ## Where data lives
78
+
79
+ All state is local to your machine, under:
80
+
81
+ ```
82
+ ~/.cyberhub-pracenv/
83
+ config.json # admin password hash + settings
84
+ profile.json # your local profile (points, solved challenges)
85
+ challenges/<id>.yml # challenge definitions
86
+ files/<id>/<file> # files attached to challenges
87
+ ```
88
+
89
+ Three sample challenges (`web-101`, `crypto-101`, `forensics-101`) are seeded on
90
+ first run.
91
+
92
+ ## Development
93
+
94
+ ```bash
95
+ npm install
96
+ npm test # runs the node:test suite
97
+ npm start # run without a global install
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Entry point for the `cyberhub-pracenv` (alias `chpe`) global command.
5
+ // Keep this thin: delegate everything to src/app.js so the binary stays a launcher.
6
+
7
+ const { main } = require('../src/app');
8
+
9
+ main(process.argv.slice(2)).catch((err) => {
10
+ console.error('Fatal error:', err && err.stack ? err.stack : err);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,16 @@
1
+ id: crypto-101
2
+ title: Caesar's Secret
3
+ category: crypto
4
+ difficulty: easy
5
+ points: 100
6
+ description: |
7
+ Intercepted transmission, encrypted with a classic rotation cipher.
8
+
9
+ The attached file (message.txt) contains ciphertext that has been shifted by
10
+ a fixed number of letters (a Caesar cipher). Recover the plaintext to reveal
11
+ the flag.
12
+
13
+ Hint: there are only 25 shifts to try.
14
+ flag: CHPE{rotation_is_not_encryption}
15
+ files:
16
+ - message.txt
@@ -0,0 +1,5 @@
1
+ Intercepted transmission (Caesar cipher, unknown shift):
2
+
3
+ Gur synt vf PUCR{ebgngvba_vf_abg_rapelcgvba}
4
+
5
+ -- end of transmission --
@@ -0,0 +1,7 @@
1
+ 10.0.0.5 - - [24/Jun/2026:09:11:02 +0000] "GET /index.html HTTP/1.1" 200 1043
2
+ 10.0.0.5 - - [24/Jun/2026:09:11:03 +0000] "GET /style.css HTTP/1.1" 200 512
3
+ 10.0.0.9 - - [24/Jun/2026:09:12:44 +0000] "POST /login HTTP/1.1" 302 0
4
+ 10.0.0.9 - - [24/Jun/2026:09:12:45 +0000] "GET /dashboard HTTP/1.1" 200 8841
5
+ 192.168.1.7 - - [24/Jun/2026:09:13:10 +0000] "GET /search?q=CHPE{grep_is_your_friend} HTTP/1.1" 200 233
6
+ 10.0.0.5 - - [24/Jun/2026:09:13:55 +0000] "GET /favicon.ico HTTP/1.1" 404 199
7
+ 10.0.0.12 - - [24/Jun/2026:09:14:21 +0000] "GET /robots.txt HTTP/1.1" 200 64
@@ -0,0 +1,11 @@
1
+ HTTP/1.1 302 Found
2
+ Date: Tue, 24 Jun 2026 14:03:11 GMT
3
+ Server: nginx/1.24.0
4
+ Set-Cookie: session=8f14e45fceea167a5a36dedd4bea2543; Path=/; HttpOnly
5
+ Set-Cookie: role=Z3Vlc3Q=; Path=/
6
+ Set-Cookie: theme=dark; Path=/
7
+ Location: /login?error=1
8
+
9
+ # Captured during a separate session, an admin response also set:
10
+ # Set-Cookie: role=Q0hQRXtjMDBraWVzX2FyZV9ub3RfZm9yX2F1dGh9; Path=/
11
+ # The "role" cookie is just base64. Decode the admin value to get the flag.
@@ -0,0 +1,14 @@
1
+ id: forensics-101
2
+ title: Hidden in Plain Sight
3
+ category: forensics
4
+ difficulty: easy
5
+ points: 150
6
+ description: |
7
+ Sometimes the flag is right there if you know where to look.
8
+
9
+ The attached log file (access.log) looks like an ordinary web server access
10
+ log, but a flag has been smuggled into one of the request lines. Scan through
11
+ it carefully — or use your favourite search tool — to find the CHPE{...} flag.
12
+ flag: CHPE{grep_is_your_friend}
13
+ files:
14
+ - access.log
@@ -0,0 +1,16 @@
1
+ id: web-101
2
+ title: Cookie Monster
3
+ category: web
4
+ difficulty: easy
5
+ points: 100
6
+ description: |
7
+ A login page trusts its own cookies a little too much.
8
+
9
+ The attached capture (cookies.txt) shows the HTTP response headers from the
10
+ site after a failed login. One of the cookies looks like it controls whether
11
+ you are an admin. Decode it and figure out the value the server expects.
12
+
13
+ The flag is the decoded admin cookie value, wrapped as CHPE{...}.
14
+ flag: CHPE{c00kies_are_not_for_auth}
15
+ files:
16
+ - cookies.txt
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "cyberhub-pracenv",
3
+ "version": "1.0.0",
4
+ "description": "A beginner-friendly, fully terminal-based cybersecurity (CTF) practice platform in the ICOA terminal style.",
5
+ "keywords": [
6
+ "cybersecurity",
7
+ "ctf",
8
+ "cli",
9
+ "terminal",
10
+ "practice",
11
+ "education",
12
+ "icoa"
13
+ ],
14
+ "license": "MIT",
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "bin": {
19
+ "cyberhub-pracenv": "bin/cyberhub.js",
20
+ "chpe": "bin/cyberhub.js"
21
+ },
22
+ "main": "src/app.js",
23
+ "files": [
24
+ "bin/",
25
+ "src/",
26
+ "data/",
27
+ "README.md"
28
+ ],
29
+ "scripts": {
30
+ "start": "node bin/cyberhub.js",
31
+ "test": "node --test"
32
+ },
33
+ "dependencies": {
34
+ "chalk": "^4.1.2",
35
+ "js-yaml": "^4.1.0"
36
+ }
37
+ }
package/src/app.js ADDED
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ // Top-level orchestration: handle non-interactive flags, initialise the data
4
+ // directory, show the welcome screen (exit by pressing Enter), then drop the
5
+ // user into the in-platform shell.
6
+
7
+ const readline = require('readline');
8
+ const pkg = require('../package.json');
9
+ const store = require('./store');
10
+ const ui = require('./ui');
11
+ const { createInput } = require('./input');
12
+ const { startRepl } = require('./repl');
13
+ const { c } = ui;
14
+
15
+ function printVersion() {
16
+ process.stdout.write(`${pkg.name} v${pkg.version}\n`);
17
+ }
18
+
19
+ function printHelp() {
20
+ process.stdout.write(
21
+ [
22
+ `${pkg.name} v${pkg.version}`,
23
+ pkg.description,
24
+ '',
25
+ 'Usage:',
26
+ ' cyberhub-pracenv Launch the interactive platform',
27
+ ' cyberhub-pracenv --help Show this help',
28
+ ' cyberhub-pracenv --version',
29
+ '',
30
+ 'Once inside, type `help` to see the in-platform commands.',
31
+ `Default admin password: "${store.DEFAULT_ADMIN_PASSWORD}" (change it with \`passwd\`).`,
32
+ '',
33
+ ].join('\n')
34
+ );
35
+ }
36
+
37
+ // Build the shared readline interface and install the echo-muting hook used by
38
+ // the masked password prompt.
39
+ function createInterface() {
40
+ const rl = readline.createInterface({
41
+ input: process.stdin,
42
+ output: process.stdout,
43
+ // Terminal mode only when attached to a real TTY; piping input (tests,
44
+ // scripts) needs standard line mode so every line is dispatched.
45
+ terminal: Boolean(process.stdin.isTTY),
46
+ });
47
+ rl.stdoutMuted = false;
48
+ rl._writeToOutput = function _writeToOutput(str) {
49
+ if (rl.stdoutMuted) return; // suppress echo during password entry
50
+ rl.output.write(str);
51
+ };
52
+ return rl;
53
+ }
54
+
55
+ async function main(argv = []) {
56
+ if (argv.includes('--version') || argv.includes('-v')) {
57
+ printVersion();
58
+ return;
59
+ }
60
+ if (argv.includes('--help') || argv.includes('-h')) {
61
+ printHelp();
62
+ return;
63
+ }
64
+
65
+ // Capture the directory the platform was launched from so `get`/`addfile`
66
+ // resolve relative paths against it.
67
+ const launchCwd = process.cwd();
68
+
69
+ store.init();
70
+
71
+ const rl = createInterface();
72
+ const input = createInput(rl);
73
+
74
+ // Welcome screen: any Enter proceeds.
75
+ process.stdout.write('\x1b[2J\x1b[H'); // clear screen
76
+ process.stdout.write(ui.welcomeScreen() + '\n');
77
+ await input.ask('');
78
+
79
+ process.stdout.write('\x1b[2J\x1b[H');
80
+ process.stdout.write(c.title('CyberHub Practice Environment') + '\n\n');
81
+
82
+ await startRepl(rl, input, launchCwd);
83
+ }
84
+
85
+ module.exports = { main };
package/src/auth.js ADDED
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ // Admin authentication: scrypt-based password hashing plus a tiny in-memory
4
+ // session flag. The platform has a single local profile; "admin" is a mode you
5
+ // unlock for the current session by entering the admin password.
6
+
7
+ const crypto = require('crypto');
8
+
9
+ const KEYLEN = 64;
10
+
11
+ function hashPassword(password, salt) {
12
+ const useSalt = salt || crypto.randomBytes(16).toString('hex');
13
+ const hash = crypto.scryptSync(String(password), useSalt, KEYLEN).toString('hex');
14
+ return { hash, salt: useSalt };
15
+ }
16
+
17
+ function verifyPassword(password, expectedHash, salt) {
18
+ if (!expectedHash || !salt) return false;
19
+ const { hash } = hashPassword(password, salt);
20
+ const a = Buffer.from(hash, 'hex');
21
+ const b = Buffer.from(expectedHash, 'hex');
22
+ if (a.length !== b.length) return false;
23
+ return crypto.timingSafeEqual(a, b);
24
+ }
25
+
26
+ // Per-session admin state.
27
+ let adminUnlocked = false;
28
+
29
+ function isAdmin() {
30
+ return adminUnlocked;
31
+ }
32
+
33
+ function setAdmin(value) {
34
+ adminUnlocked = Boolean(value);
35
+ }
36
+
37
+ module.exports = {
38
+ hashPassword,
39
+ verifyPassword,
40
+ isAdmin,
41
+ setAdmin,
42
+ };