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.
@@ -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>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="GoImports">
4
+ <option name="excludedPackages">
5
+ <array>
6
+ <option value="golang.org/x/net/context" />
7
+ </array>
8
+ </option>
9
+ </component>
10
+ </project>
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
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
@@ -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":"AAWA,qBAAa,UAAU;IACnB,OAAO,CAAC,OAAO,CAAiB;IAIhC,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,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA2BhD,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,kBAAkB;IA4BpB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;IA0E5C,eAAe;CAclB"}
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"}
@@ -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 styleTagRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
56
- return html.replace(styleTagRegex, (_, p1) => {
57
- let styleContent = p1;
58
- styleContent = this.obfuscateCSS(styleContent);
59
- return `<style>${styleContent}</style>`;
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,3 @@
1
+ import * as t from '@babel/types';
2
+ export declare function isDomElement(node: t.Node, scope: any): boolean;
3
+ //# sourceMappingURL=javascript-obfuscator.d.ts.map
@@ -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.1",
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",