css-shuffle 1.0.1 → 1.1.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/.idea/css-shuffle.iml +9 -0
- package/.idea/go.imports.xml +10 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/dist/css-shuffle.d.ts +2 -0
- package/dist/css-shuffle.d.ts.map +1 -1
- package/dist/css-shuffle.js +163 -10
- package/dist/javascript-obfuscator.d.ts +3 -0
- package/dist/javascript-obfuscator.d.ts.map +1 -0
- package/dist/javascript-obfuscator.js +84 -0
- package/package.json +7 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="JAVA_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
4
|
+
<exclude-output />
|
|
5
|
+
<content url="file://$MODULE_DIR$" />
|
|
6
|
+
<orderEntry type="inheritedJdk" />
|
|
7
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
8
|
+
</component>
|
|
9
|
+
</module>
|
package/.idea/misc.xml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
|
4
|
+
<output url="file://$PROJECT_DIR$/out" />
|
|
5
|
+
</component>
|
|
6
|
+
</project>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/css-shuffle.iml" filepath="$PROJECT_DIR$/.idea/css-shuffle.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
package/.idea/vcs.xml
ADDED
package/dist/css-shuffle.d.ts
CHANGED
|
@@ -6,8 +6,10 @@ export declare class CSSShuffle {
|
|
|
6
6
|
getMapping(): Map<string, string>;
|
|
7
7
|
getMappingJSON(): string;
|
|
8
8
|
saveMappingJSON(path: string): void;
|
|
9
|
+
obfuscateJS(js: string): Promise<string>;
|
|
9
10
|
obfuscateCSS(css: string): Promise<string>;
|
|
10
11
|
private obfuscateCSSInHtml;
|
|
12
|
+
private obfuscateSelector;
|
|
11
13
|
private replaceNamesInHtml;
|
|
12
14
|
obfuscate(input: string, dist?: string): Promise<void>;
|
|
13
15
|
printStatsTable(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"css-shuffle.d.ts","sourceRoot":"","sources":["../src/css-shuffle.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"css-shuffle.d.ts","sourceRoot":"","sources":["../src/css-shuffle.ts"],"names":[],"mappings":"AAmBA,qBAAa,UAAU;IACnB,OAAO,CAAC,OAAO,CAAiB;IAEhC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4D;IAElF,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,gBAAgB;IAIxB,UAAU,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAIjC,cAAc,IAAI,MAAM;IAIxB,eAAe,CAAC,IAAI,EAAE,MAAM;IAMtB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoIxC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YA2BlC,kBAAkB;IAehC,OAAO,CAAC,iBAAiB;YAYX,kBAAkB;IAsC1B,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;IA0E5C,eAAe;CAclB"}
|
package/dist/css-shuffle.js
CHANGED
|
@@ -6,10 +6,16 @@ import prettyBytes from "pretty-bytes";
|
|
|
6
6
|
import postcss, { Root } from "postcss";
|
|
7
7
|
import selectorParser from "postcss-selector-parser";
|
|
8
8
|
import valueParser from "postcss-value-parser";
|
|
9
|
+
import * as parser from '@babel/parser';
|
|
10
|
+
import _traverse from '@babel/traverse';
|
|
11
|
+
const traverse = (_traverse).default;
|
|
12
|
+
import _generate from '@babel/generator';
|
|
13
|
+
const generate = (_generate).default;
|
|
14
|
+
import * as t from '@babel/types';
|
|
9
15
|
import { Renamer } from "./renamer.js";
|
|
16
|
+
import { isDomElement } from "./javascript-obfuscator.js";
|
|
10
17
|
export class CSSShuffle {
|
|
11
18
|
renamer = new Renamer();
|
|
12
|
-
// private readonly stats = new Table();
|
|
13
19
|
stats = new Map();
|
|
14
20
|
obfuscateName(originalName) {
|
|
15
21
|
return this.renamer.rename(originalName);
|
|
@@ -27,6 +33,127 @@ export class CSSShuffle {
|
|
|
27
33
|
const mapping = this.getMappingJSON();
|
|
28
34
|
fs.writeFileSync(path, mapping);
|
|
29
35
|
}
|
|
36
|
+
async obfuscateJS(js) {
|
|
37
|
+
const ast = parser.parse(js, {
|
|
38
|
+
sourceType: 'script',
|
|
39
|
+
plugins: ['classProperties'],
|
|
40
|
+
errorRecovery: true,
|
|
41
|
+
});
|
|
42
|
+
const getStringValue = (node) => {
|
|
43
|
+
if (t.isStringLiteral(node))
|
|
44
|
+
return node.value;
|
|
45
|
+
if (t.isTemplateLiteral(node) && node.quasis.length === 1 && node.expressions.length === 0) {
|
|
46
|
+
return node.quasis[0].value.cooked || node.quasis[0].value.raw;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
const createStringNode = (originalNode, value) => {
|
|
51
|
+
if (t.isTemplateLiteral(originalNode)) {
|
|
52
|
+
return t.templateLiteral([t.templateElement({ raw: value, cooked: value }, true)], []);
|
|
53
|
+
}
|
|
54
|
+
return t.stringLiteral(value);
|
|
55
|
+
};
|
|
56
|
+
traverse(ast, {
|
|
57
|
+
CallExpression: (path) => {
|
|
58
|
+
const { callee, arguments: args } = path.node;
|
|
59
|
+
if (!t.isMemberExpression(callee))
|
|
60
|
+
return;
|
|
61
|
+
const object = callee.object;
|
|
62
|
+
const method = callee.property;
|
|
63
|
+
if (!t.isIdentifier(method))
|
|
64
|
+
return;
|
|
65
|
+
// ── classList.add/remove/toggle/contains/replace ──────────────────
|
|
66
|
+
if (t.isMemberExpression(object) &&
|
|
67
|
+
t.isIdentifier(object.property, { name: 'classList' }) &&
|
|
68
|
+
['add', 'remove', 'toggle', 'contains', 'replace'].includes(method.name) &&
|
|
69
|
+
isDomElement(object.object, path.scope) // ← guard
|
|
70
|
+
) {
|
|
71
|
+
args.forEach((arg, i) => {
|
|
72
|
+
const val = getStringValue(arg);
|
|
73
|
+
if (val !== null) {
|
|
74
|
+
const obf = this.getObfuscateName(val);
|
|
75
|
+
if (obf)
|
|
76
|
+
args[i] = createStringNode(arg, obf);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// ── querySelector / querySelectorAll ──────────────────────────────
|
|
81
|
+
if (['querySelector', 'querySelectorAll'].includes(method.name) &&
|
|
82
|
+
args.length === 1 &&
|
|
83
|
+
isDomElement(object, path.scope) // ← guard
|
|
84
|
+
) {
|
|
85
|
+
const val = getStringValue(args[0]);
|
|
86
|
+
if (val !== null) {
|
|
87
|
+
args[0] = createStringNode(args[0], this.obfuscateSelector(val));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// ── getElementById ────────────────────────────────────────────────
|
|
91
|
+
if (method.name === 'getElementById' &&
|
|
92
|
+
args.length === 1 &&
|
|
93
|
+
isDomElement(object, path.scope) // ← guard
|
|
94
|
+
) {
|
|
95
|
+
const val = getStringValue(args[0]);
|
|
96
|
+
if (val !== null) {
|
|
97
|
+
const obf = this.getObfuscateName(val);
|
|
98
|
+
if (obf)
|
|
99
|
+
args[0] = createStringNode(args[0], obf);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ── getElementsByClassName ────────────────────────────────────────
|
|
103
|
+
if (method.name === 'getElementsByClassName' &&
|
|
104
|
+
args.length === 1 &&
|
|
105
|
+
isDomElement(object, path.scope) // ← guard
|
|
106
|
+
) {
|
|
107
|
+
const val = getStringValue(args[0]);
|
|
108
|
+
if (val !== null) {
|
|
109
|
+
const obf = this.getObfuscateName(val);
|
|
110
|
+
if (obf)
|
|
111
|
+
args[0] = createStringNode(args[0], obf);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ── setAttribute('class'/'id', ...) ───────────────────────────────
|
|
115
|
+
if (method.name === 'setAttribute' &&
|
|
116
|
+
args.length === 2 &&
|
|
117
|
+
isDomElement(object, path.scope) // ← guard
|
|
118
|
+
) {
|
|
119
|
+
const attrName = getStringValue(args[0]);
|
|
120
|
+
const attrVal = getStringValue(args[1]);
|
|
121
|
+
if (attrName !== null && attrVal !== null) {
|
|
122
|
+
if (attrName === 'class') {
|
|
123
|
+
const newVal = attrVal
|
|
124
|
+
.split(/\s+/)
|
|
125
|
+
.map(cls => this.getObfuscateName(cls) || cls)
|
|
126
|
+
.join(' ');
|
|
127
|
+
args[1] = createStringNode(args[1], newVal);
|
|
128
|
+
}
|
|
129
|
+
else if (attrName === 'id') {
|
|
130
|
+
const obf = this.getObfuscateName(attrVal);
|
|
131
|
+
if (obf)
|
|
132
|
+
args[1] = createStringNode(args[1], obf);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
// ── element.className = 'foo bar' ─────────────────────────────────────
|
|
138
|
+
AssignmentExpression: (path) => {
|
|
139
|
+
const { left, right } = path.node;
|
|
140
|
+
if (t.isMemberExpression(left) &&
|
|
141
|
+
t.isIdentifier(left.property, { name: 'className' }) &&
|
|
142
|
+
isDomElement(left.object, path.scope) // ← guard
|
|
143
|
+
) {
|
|
144
|
+
const val = getStringValue(right);
|
|
145
|
+
if (val !== null) {
|
|
146
|
+
const newVal = val
|
|
147
|
+
.split(/\s+/)
|
|
148
|
+
.map(cls => this.getObfuscateName(cls) || cls)
|
|
149
|
+
.join(' ');
|
|
150
|
+
path.node.right = createStringNode(right, newVal);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
return generate(ast, { retainLines: true }, js).code;
|
|
156
|
+
}
|
|
30
157
|
async obfuscateCSS(css) {
|
|
31
158
|
return await postcss([
|
|
32
159
|
(root) => {
|
|
@@ -51,15 +178,32 @@ export class CSSShuffle {
|
|
|
51
178
|
}
|
|
52
179
|
]).process(css, { from: undefined }).then(result => result.css);
|
|
53
180
|
}
|
|
54
|
-
obfuscateCSSInHtml(html) {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
181
|
+
async obfuscateCSSInHtml(html) {
|
|
182
|
+
const $ = cheerio.load(html);
|
|
183
|
+
const styles = $('style').toArray();
|
|
184
|
+
for (const style of styles) {
|
|
185
|
+
const $style = $(style);
|
|
186
|
+
const content = $style.html();
|
|
187
|
+
if (content) {
|
|
188
|
+
const obfuscatedContent = await this.obfuscateCSS(content);
|
|
189
|
+
$style.html(obfuscatedContent);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return $.html();
|
|
193
|
+
}
|
|
194
|
+
// Reuse your existing CSS selector obfuscation logic
|
|
195
|
+
obfuscateSelector(selector) {
|
|
196
|
+
return selector
|
|
197
|
+
.replace(/\.([a-zA-Z0-9_-]+)/g, (_, cls) => {
|
|
198
|
+
const obf = this.getObfuscateName(cls);
|
|
199
|
+
return obf ? `.${obf}` : `.${cls}`;
|
|
200
|
+
})
|
|
201
|
+
.replace(/#([a-zA-Z0-9_-]+)/g, (_, id) => {
|
|
202
|
+
const obf = this.getObfuscateName(id);
|
|
203
|
+
return obf ? `#${obf}` : `#${id}`;
|
|
60
204
|
});
|
|
61
205
|
}
|
|
62
|
-
replaceNamesInHtml(html) {
|
|
206
|
+
async replaceNamesInHtml(html) {
|
|
63
207
|
const $ = cheerio.load(html);
|
|
64
208
|
$('[class]').each((_, e) => {
|
|
65
209
|
const classes = $(e).attr('class').split(/\s+/).filter(Boolean);
|
|
@@ -79,6 +223,15 @@ export class CSSShuffle {
|
|
|
79
223
|
const target = href.slice(1);
|
|
80
224
|
$(e).attr('href', '#' + this.getObfuscateName(target));
|
|
81
225
|
});
|
|
226
|
+
const scripts = $('script').toArray();
|
|
227
|
+
for (const script of scripts) {
|
|
228
|
+
const $script = $(script);
|
|
229
|
+
const content = $script.html();
|
|
230
|
+
if (content) {
|
|
231
|
+
const obfuscatedContent = await this.obfuscateJS(content);
|
|
232
|
+
$script.html(obfuscatedContent);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
82
235
|
return $.html();
|
|
83
236
|
}
|
|
84
237
|
async obfuscate(input, dist) {
|
|
@@ -113,7 +266,7 @@ export class CSSShuffle {
|
|
|
113
266
|
// Obfuscate CSS in <style> tag in HTML files
|
|
114
267
|
for (const htmlFile of htmlFiles) {
|
|
115
268
|
const htmlContent = fs.readFileSync(htmlFile, 'utf-8');
|
|
116
|
-
let obfuscatedHtmlContent = this.obfuscateCSSInHtml(htmlContent);
|
|
269
|
+
let obfuscatedHtmlContent = await this.obfuscateCSSInHtml(htmlContent);
|
|
117
270
|
fs.writeFileSync(htmlFile, obfuscatedHtmlContent, 'utf-8');
|
|
118
271
|
const oldSize = htmlContent.length;
|
|
119
272
|
const newSize = obfuscatedHtmlContent.length;
|
|
@@ -128,7 +281,7 @@ export class CSSShuffle {
|
|
|
128
281
|
// Export export obfuscated names to HTML
|
|
129
282
|
for (const htmlFile of htmlFiles) {
|
|
130
283
|
const htmlContent = fs.readFileSync(htmlFile, 'utf-8');
|
|
131
|
-
let newHtmlContent = this.replaceNamesInHtml(htmlContent);
|
|
284
|
+
let newHtmlContent = await this.replaceNamesInHtml(htmlContent);
|
|
132
285
|
fs.writeFileSync(htmlFile, newHtmlContent, 'utf-8');
|
|
133
286
|
let orginalSize = htmlContent.length;
|
|
134
287
|
const newSize = newHtmlContent.length;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"javascript-obfuscator.d.ts","sourceRoot":"","sources":["../src/javascript-obfuscator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAiClC,wBAAgB,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAkE9D"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
// DOM-producing methods that return elements
|
|
3
|
+
const DOM_ELEMENT_SOURCES = new Set([
|
|
4
|
+
'getElementById',
|
|
5
|
+
'getElementsByClassName',
|
|
6
|
+
'getElementsByTagName',
|
|
7
|
+
'getElementsByName',
|
|
8
|
+
'querySelector',
|
|
9
|
+
'querySelectorAll',
|
|
10
|
+
'createElement',
|
|
11
|
+
'createElementNS',
|
|
12
|
+
'closest',
|
|
13
|
+
'parentElement',
|
|
14
|
+
'firstElementChild',
|
|
15
|
+
'lastElementChild',
|
|
16
|
+
'nextElementSibling',
|
|
17
|
+
'previousElementSibling',
|
|
18
|
+
]);
|
|
19
|
+
// Properties on DOM elements that return elements
|
|
20
|
+
const DOM_ELEMENT_PROPERTIES = new Set([
|
|
21
|
+
'parentElement',
|
|
22
|
+
'firstElementChild',
|
|
23
|
+
'lastElementChild',
|
|
24
|
+
'nextElementSibling',
|
|
25
|
+
'previousElementSibling',
|
|
26
|
+
'ownerDocument',
|
|
27
|
+
'body',
|
|
28
|
+
'head',
|
|
29
|
+
'documentElement',
|
|
30
|
+
]);
|
|
31
|
+
export function isDomElement(node, scope) {
|
|
32
|
+
// document.getElementById(...) / document.querySelector(...) inline
|
|
33
|
+
if (t.isCallExpression(node)) {
|
|
34
|
+
const callee = node.callee;
|
|
35
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
36
|
+
// document.X() or element.X()
|
|
37
|
+
if (DOM_ELEMENT_SOURCES.has(callee.property.name)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
// document.body / document.head / document.documentElement
|
|
44
|
+
if (t.isMemberExpression(node)) {
|
|
45
|
+
if (t.isIdentifier(node.property) && DOM_ELEMENT_PROPERTIES.has(node.property.name)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Identifier — look up what it was assigned from
|
|
51
|
+
if (t.isIdentifier(node)) {
|
|
52
|
+
// Well-known globals
|
|
53
|
+
if (['document', 'window', 'HTMLElement'].includes(node.name)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const binding = scope.getBinding(node.name);
|
|
57
|
+
if (!binding)
|
|
58
|
+
return false;
|
|
59
|
+
const bindingPath = binding.path;
|
|
60
|
+
// const el = document.querySelector(...)
|
|
61
|
+
// const el = someEl.closest(...)
|
|
62
|
+
if (t.isVariableDeclarator(bindingPath.node) && bindingPath.node.init) {
|
|
63
|
+
return isDomElement(bindingPath.node.init, bindingPath.scope);
|
|
64
|
+
}
|
|
65
|
+
// function param: forEach(link => link.addEventListener...)
|
|
66
|
+
// treat params of callbacks whose parent array is DOM-sourced
|
|
67
|
+
if (bindingPath.node.type === 'Identifier' && bindingPath.parentPath) {
|
|
68
|
+
const parent = bindingPath.parentPath.node;
|
|
69
|
+
// Arrow/function param in .forEach/.map etc on a DOM NodeList/array
|
|
70
|
+
if (t.isArrowFunctionExpression(parent) || t.isFunctionExpression(parent)) {
|
|
71
|
+
const callPath = bindingPath.parentPath.parentPath;
|
|
72
|
+
if (callPath &&
|
|
73
|
+
t.isCallExpression(callPath.node) &&
|
|
74
|
+
t.isMemberExpression(callPath.node.callee) &&
|
|
75
|
+
t.isIdentifier(callPath.node.callee.property) &&
|
|
76
|
+
['forEach', 'map', 'filter', 'find'].includes(callPath.node.callee.property.name)) {
|
|
77
|
+
// Check if the array/NodeList being iterated is DOM-sourced
|
|
78
|
+
return isDomElement(callPath.node.callee.object, callPath.scope);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "css-shuffle",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Tool for obfuscating and randomizing all CSS classes and ID names in your HTML, CSS files.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://github.com/M3DZIK/css-shuffle#readme",
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"@babel/generator": "^7.29.1",
|
|
22
|
+
"@babel/parser": "^7.29.2",
|
|
23
|
+
"@babel/traverse": "^7.29.0",
|
|
24
|
+
"@babel/types": "^7.29.0",
|
|
21
25
|
"cheerio": "^1.1.2",
|
|
22
26
|
"console-table-printer": "^2.14.6",
|
|
23
27
|
"globby": "^14.1.0",
|
|
@@ -27,6 +31,8 @@
|
|
|
27
31
|
"pretty-bytes": "^7.0.1"
|
|
28
32
|
},
|
|
29
33
|
"devDependencies": {
|
|
34
|
+
"@types/babel__generator": "^7.27.0",
|
|
35
|
+
"@types/babel__traverse": "^7.28.0",
|
|
30
36
|
"@types/css-tree": "^2.3.10",
|
|
31
37
|
"@types/node": "^24.3.0",
|
|
32
38
|
"astro": "^5.13.3",
|