cyberhub-pracenv 1.1.0 → 1.1.1
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/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/src/commands.js +87 -0
- package/src/ui.js +25 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to **cyberhub-pracenv** (`chpe`) are documented in this file
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.1] - 2026-06-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`changelog` command** — view this changelog from inside the platform so you
|
|
12
|
+
can see what has changed across versions without leaving the shell.
|
|
13
|
+
- **`requirements` command (alias `rq`)** — show the rank ladder and how many
|
|
14
|
+
more points are needed to reach the next rank. The rank thresholds now live in
|
|
15
|
+
one place (`ui.RANKS`) shared by `rankFor` and this view.
|
|
16
|
+
- **`scoreboard` command** — an operative leaderboard listing every user with
|
|
17
|
+
their rank, points, and solved count. A secret `sb` shortcut is also available.
|
|
18
|
+
- A secret `ch` shortcut for the `challenges`/`ls` listing.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `help` now lists the `changelog`, `requirements`, and `scoreboard` commands.
|
|
22
|
+
The `ch` and `sb` shortcuts are intentionally left undocumented.
|
|
23
|
+
|
|
8
24
|
## [1.1.0] - 2026-06-25
|
|
9
25
|
|
|
10
26
|
### Fixed
|
package/package.json
CHANGED
package/src/commands.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// The command table at the bottom drives both dispatch and the `help` listing.
|
|
8
8
|
|
|
9
|
+
const fs = require('fs');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const store = require('./store');
|
|
11
12
|
const auth = require('./auth');
|
|
@@ -38,6 +39,9 @@ function cmdHelp(ctx) {
|
|
|
38
39
|
['get <id> <file>', 'Copy an attached file into your launch directory'],
|
|
39
40
|
['submit <id> <flag>', 'Submit a flag to solve a challenge'],
|
|
40
41
|
['stats (whoami)', 'Show your full user breakdown'],
|
|
42
|
+
['scoreboard', 'Show the operative leaderboard'],
|
|
43
|
+
['requirements (rq)', 'Show the points needed to rank up'],
|
|
44
|
+
['changelog', 'Show what has changed across versions'],
|
|
41
45
|
['admin', 'Unlock admin mode (requires the admin password)'],
|
|
42
46
|
['clear', 'Clear the screen'],
|
|
43
47
|
['help', 'Show this help'],
|
|
@@ -445,6 +449,83 @@ function cmdClear(ctx) {
|
|
|
445
449
|
print(ctx, c.title('CyberHub Practice Environment') + c.dim(' — type `help` for commands'));
|
|
446
450
|
}
|
|
447
451
|
|
|
452
|
+
// Show the bundled CHANGELOG.md so operatives can see what has changed without
|
|
453
|
+
// leaving the platform. The markdown is lightly styled for the terminal.
|
|
454
|
+
function cmdChangelog(ctx) {
|
|
455
|
+
const file = path.join(__dirname, '..', 'CHANGELOG.md');
|
|
456
|
+
let text;
|
|
457
|
+
try {
|
|
458
|
+
text = fs.readFileSync(file, 'utf8');
|
|
459
|
+
} catch (_err) {
|
|
460
|
+
print(ctx, c.err('Changelog is not available.'));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
print(ctx, c.title('Changelog'));
|
|
464
|
+
for (const line of text.replace(/\s+$/, '').split('\n')) {
|
|
465
|
+
if (/^#{1,6}\s/.test(line)) {
|
|
466
|
+
print(ctx, c.accent(line.replace(/^#{1,6}\s+/, '')));
|
|
467
|
+
} else if (/^\s*[-*]\s/.test(line)) {
|
|
468
|
+
print(ctx, c.dim(' • ') + line.replace(/^\s*[-*]\s+/, ''));
|
|
469
|
+
} else {
|
|
470
|
+
print(ctx, line);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Show the rank ladder and how far the operative is from the next rank.
|
|
476
|
+
function cmdRequirements(ctx) {
|
|
477
|
+
const profile = store.getProfile();
|
|
478
|
+
const points = profile.points || 0;
|
|
479
|
+
const current = ui.rankFor(points);
|
|
480
|
+
|
|
481
|
+
print(ctx, c.title('Rank requirements'));
|
|
482
|
+
const rows = ui.RANKS.map((r) => ({
|
|
483
|
+
cur: r.name === current ? '➤' : ' ',
|
|
484
|
+
rank: r.name,
|
|
485
|
+
points: r.min,
|
|
486
|
+
}));
|
|
487
|
+
print(ctx, ui.table(rows, [
|
|
488
|
+
{ key: 'cur', label: ' ', color: c.ok },
|
|
489
|
+
{ key: 'rank', label: 'Rank', color: c.accent },
|
|
490
|
+
{ key: 'points', label: 'Min points' },
|
|
491
|
+
]));
|
|
492
|
+
|
|
493
|
+
print(ctx, '');
|
|
494
|
+
print(ctx, ` ${c.bold('You')} : ${c.accent(current)} with ${c.ok(points)} pts`);
|
|
495
|
+
const next = ui.nextRank(points);
|
|
496
|
+
if (next) {
|
|
497
|
+
print(ctx, c.dim(` ${next.min - points} more point(s) to reach ${next.name}.`));
|
|
498
|
+
} else {
|
|
499
|
+
print(ctx, c.dim(' You have reached the top rank. Outstanding.'));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Show a leaderboard of operatives. The platform uses a single local profile,
|
|
504
|
+
// so the scoreboard ranks that operative — but it is built to render any number
|
|
505
|
+
// of profiles so it keeps working if more are ever added.
|
|
506
|
+
function cmdScoreboard(ctx) {
|
|
507
|
+
const total = store.listChallenges().length;
|
|
508
|
+
const users = [store.getProfile()];
|
|
509
|
+
const rows = users
|
|
510
|
+
.slice()
|
|
511
|
+
.sort((a, b) => (b.points || 0) - (a.points || 0))
|
|
512
|
+
.map((u, i) => ({
|
|
513
|
+
pos: i + 1,
|
|
514
|
+
name: u.name,
|
|
515
|
+
rank: ui.rankFor(u.points || 0),
|
|
516
|
+
points: u.points || 0,
|
|
517
|
+
solved: `${(u.solved || []).length}/${total}`,
|
|
518
|
+
}));
|
|
519
|
+
print(ctx, c.title('Scoreboard'));
|
|
520
|
+
print(ctx, ui.table(rows, [
|
|
521
|
+
{ key: 'pos', label: '#', color: c.dim },
|
|
522
|
+
{ key: 'name', label: 'Operative', color: c.accent },
|
|
523
|
+
{ key: 'rank', label: 'Rank' },
|
|
524
|
+
{ key: 'points', label: 'Pts', color: c.ok },
|
|
525
|
+
{ key: 'solved', label: 'Solved' },
|
|
526
|
+
]));
|
|
527
|
+
}
|
|
528
|
+
|
|
448
529
|
function cmdExit(ctx) {
|
|
449
530
|
print(ctx, c.dim('Stay sharp. Goodbye.'));
|
|
450
531
|
ctx.requestExit();
|
|
@@ -459,6 +540,7 @@ const COMMANDS = {
|
|
|
459
540
|
'?': cmdHelp,
|
|
460
541
|
challenges: cmdChallenges,
|
|
461
542
|
ls: cmdChallenges,
|
|
543
|
+
ch: cmdChallenges, // secret shortcut (intentionally undocumented)
|
|
462
544
|
open: cmdOpen,
|
|
463
545
|
show: cmdOpen,
|
|
464
546
|
files: cmdFiles,
|
|
@@ -469,6 +551,11 @@ const COMMANDS = {
|
|
|
469
551
|
stats: cmdStats,
|
|
470
552
|
whoami: cmdStats,
|
|
471
553
|
profile: cmdStats,
|
|
554
|
+
scoreboard: cmdScoreboard,
|
|
555
|
+
sb: cmdScoreboard, // secret shortcut (intentionally undocumented)
|
|
556
|
+
requirements: cmdRequirements,
|
|
557
|
+
rq: cmdRequirements,
|
|
558
|
+
changelog: cmdChangelog,
|
|
472
559
|
admin: cmdAdmin,
|
|
473
560
|
login: cmdAdmin,
|
|
474
561
|
logout: cmdLogout,
|
package/src/ui.js
CHANGED
|
@@ -95,12 +95,30 @@ function progressBar(done, total, width = 24) {
|
|
|
95
95
|
return `${bar} ${safeDone}/${safeTotal} ${c.dim(`(${pct}%)`)}`;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// The rank ladder: the minimum points required to hold each rank, ascending.
|
|
99
|
+
// rankFor() and the in-platform `requirements` command both read from this so
|
|
100
|
+
// the thresholds live in exactly one place.
|
|
101
|
+
const RANKS = [
|
|
102
|
+
{ name: 'Recruit', min: 0 },
|
|
103
|
+
{ name: 'Script Kiddie', min: 100 },
|
|
104
|
+
{ name: 'Pentester', min: 300 },
|
|
105
|
+
{ name: 'Operator', min: 600 },
|
|
106
|
+
{ name: 'Elite', min: 1000 },
|
|
107
|
+
];
|
|
108
|
+
|
|
98
109
|
function rankFor(points) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
const pts = Number(points) || 0;
|
|
111
|
+
let rank = RANKS[0].name;
|
|
112
|
+
for (const r of RANKS) {
|
|
113
|
+
if (pts >= r.min) rank = r.name;
|
|
114
|
+
}
|
|
115
|
+
return rank;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The next rank above the given points, or null if already at the top.
|
|
119
|
+
function nextRank(points) {
|
|
120
|
+
const pts = Number(points) || 0;
|
|
121
|
+
return RANKS.find((r) => r.min > pts) || null;
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
module.exports = {
|
|
@@ -109,5 +127,7 @@ module.exports = {
|
|
|
109
127
|
welcomeScreen,
|
|
110
128
|
table,
|
|
111
129
|
progressBar,
|
|
130
|
+
RANKS,
|
|
112
131
|
rankFor,
|
|
132
|
+
nextRank,
|
|
113
133
|
};
|