Lexical maps
JSDefender, by default, creates a lexical-map.json
file that reflects the hierarchy of lexical scopes with declarations, resolved and unresolved references to identifiers. With the -m
or --mapout
command line options, or the mapFile
configuration property, you can change the location and name of the map file, or even turn it off. Here are a few examples:
Let's name the map file to my-map.json
and move it to the maps
folder:
With CLI:
jsdefender -m ./map/my-map.json
jsdefender --mapout ./map/my-map.json
In configuration file:
{
// ...
"mapFile": "./map/my-map.json"
// ...
}
Let's turn off creating the lexical map:
With CLI:
jsdefender -m off
jsdefender --mapout off
In configuration file:
{
// ...
"mapFile": "off"
// ...
}
Lexical map format
The lexical map is a JSON file. The easiest way to understand its structure is to see how it looks for concrete protection.
Let's assume the protection includes these files:
file1.js
:
function getGreeting() {
var name = getName();
return "Hello, " + name + ", from multifile demo!";
}
file2.js
:
function getName() {
return "Developer";
}
var element = document.getElementById("msg");
element.textContent = getGreeting();
This is the configuration file we use:
jsdefender.config.json
:
{
"inputs": ["file1.js", "file2.js"],
"settings": {
"localDeclarations": {
"nameMangling": "sequential"
},
"stringLiterals": false
}
}
After running JSDefender, these are the protected files (you can see the identifiers renamed):
file1.js
:
function _0x000000() {
var _0x000001 = _0x000002(); // ID declared in file2.js
return "Hello, " + _0x000001 + ", from multifile demo!";
}
file2.js
:
function _0x000002() {
// file1.js has a reference to this ID
return "Developer";
}
var _0x000003 = document.getElementById("msg");
_0x000003.textContent = _0x000000();
And, here is the lexical-map.json
file:
{
"0:C:\\demos\\file1.js": {
"declarations": [
{
"globalId": 0,
"localId": 0,
"name": "getGreeting",
"line": 1,
"column": 9,
"mangledName": "_0x000000",
"type": "FunctionDeclaration",
"referenceCount": 2,
"nestedReferenceCount": 0,
"tree": 0
},
{
"globalId": 2,
"localId": 1,
"name": "getName",
"line": 1,
"column": 9,
"mangledName": "_0x000002",
"type": "FunctionDeclaration",
"referenceCount": 2,
"nestedReferenceCount": 1,
"tree": 1
},
{
"globalId": 3,
"localId": 2,
"name": "element",
"line": 5,
"column": 4,
"mangledName": "_0x000003",
"type": "Var",
"referenceCount": 2,
"nestedReferenceCount": 0,
"tree": 1
}
],
"references": [
{
"id": "getGreeting",
"globalId": 0,
"line": 1,
"column": 9
}
],
"unresolved": [],
"nested": [
{
"declarations": [
{
"globalId": 1,
"localId": 0,
"name": "name",
"line": 2,
"column": 8,
"mangledName": "_0x000001",
"type": "Var",
"referenceCount": 2,
"nestedReferenceCount": 0,
"tree": 0
}
],
"references": [
{
"id": "name",
"globalId": 1,
"line": 2,
"column": 8
},
{
"id": "getName",
"globalId": 2,
"line": 2,
"column": 15
},
{
"id": "name",
"globalId": 1,
"line": 3,
"column": 22
}
],
"unresolved": [],
"nested": []
}
]
},
"1:C:\\demos\\file2.js": {
"declarations": "inherit",
"references": [
{
"id": "getName",
"globalId": 2,
"line": 1,
"column": 9
},
{
"id": "element",
"globalId": 3,
"line": 5,
"column": 4
},
{
"id": "element",
"globalId": 3,
"line": 6,
"column": 0
},
{
"id": "getGreeting",
"globalId": 0,
"line": 6,
"column": 22
}
],
"unresolved": [
{
"id": "document",
"line": 5,
"column": 14
}
],
"nested": [
{
"declarations": [],
"references": [],
"unresolved": [],
"nested": []
}
]
}
}
This file is long. Here is its simplified structure:
{
"0:C:\\demos\\file1.js": { // First source file
"declarations": [ /* ... */ ], // Global (top-level) declarations
"references": [ /* ... */ ], // Resolved references
"unresolved": [], // Unresolved references
"nested": [ /* ... */ ] // Nested lexical scopes
},
"1:C:\\demos\\file2.js": { // Second source file
"declarations": "inherit", // Inherits declarations
"references": [ /* ... */ ], // Resolved references
"unresolved": [], // Unresolved references
"nested": [ /* ... */ ] // Nested lexical scopes
}
}
Files and global declarations
The map file has a property for each source file, which is composed of the ordinal number (index) of the source files and its absolute path. Each file has these lists:
declarations
: All declarations within the particular file.references
: All resolved references in the source file. A resolved reference means that JSDefender could connect an identifier successfully with its declaration (in any file, any lexical scope).unresolved
: All unresolved references in the source file. When JSDefender cannot find an identifier within the declarations of any file, it is marked as unresolved. Global objects, such asconsole
,window
,document
,process
, etc. get into this list.nested
: The lexical scopes nested into the global lexical scope. Within this node, there's a recursive hierarchy of the four-tuple withdeclarations
,references
,unresolved
, andnested
.
The top-level (global) lexical scope is managed differently from others. The declarations
section of the first file contains all top level declarations, reflecting the fact that JavaScript hoists all program-level declarations into the global scope. The "inherit"
values of the subsequent files' declarations
section indicates that those declarations are in the first file's scope.
Declaration items
Each entry in the declarations
list contains several properties, as this JSON snippet shows:
{
"0:C:\\demos\\file1.js": {
"declarations": [
{
"globalId": 0,
"localId": 0,
"name": "getGreeting",
"line": 1,
"column": 9,
"mangledName": "_0x000000",
"type": "FunctionDeclaration",
"referenceCount": 2,
"nestedReferenceCount": 0,
"tree": 0
},
{
"globalId": 2,
"localId": 1,
"name": "getName",
"line": 1,
"column": 9,
"mangledName": "_0x000002",
"type": "FunctionDeclaration",
"referenceCount": 2,
"nestedReferenceCount": 1,
"tree": 1
},
// ...
],
// ...
}
globalId
: Each declaration has a global identifier (zero-based sequential number) that is unique in the entire source code. This field indicates this value. Name mangling uses this value.localId
: Each declaration has a scope-specific identifier (zero-based sequential number) that is unique within the hosting scope. This field indicates this value. Name mangling uses this value.name
: The original name of the declaration before renaming.line
: The source code line number of the declaration identifier.column
: The source code column number of the declaration identifier.mangledName
: If declaration renaming is applied, this property shows the new name.type
: Type of the declaration.referenceCount
: The total number of references (either resolved or unresolved) to the declaration.nestedReferenceCount
: The total number of references (either resolved or unresolved) to the declaration from within its nested scopes.tree
: The identifier of the source file (tree) that has the declaration.
Resolved and unresolved references
When building the lexical map, JSDefender connects all declarations with corresponding identifier references. When it maps all declarations and references, the protection engine tries to resolve references and bind them to their associated declaration. When this process is successful, a reference is declared resolved, and it is bound to its declaration though the global ID. If no declaration is found for a reference, it remains unresolved.
Resolved references have these properties:
name
: The name of the referenced declarationglobalId
: The global identifier of the associated declarationline
: The source code line number of the reference identifier.column
: The source code column number of the reference identifier.
Unresolved references have the same properties, except they do not hold a globalId
property.
This snippet shows the references in file2.js
:
{
"1:C:\\demos\\file2.js": {
"declarations": "inherit",
"references": [
{
"id": "getName",
"globalId": 2,
"line": 1,
"column": 9
},
{
"id": "element",
"globalId": 3,
"line": 5,
"column": 4
},
{
"id": "element",
"globalId": 3,
"line": 6,
"column": 0
},
{
"id": "getGreeting",
"globalId": 0,
"line": 6,
"column": 22
}
],
"unresolved": [
{
"id": "document", // document could not be resolved
"line": 5,
"column": 14
}
],
"nested": [ /* ... */ ]
}
}
You can see that resolved references point to their declarations through the globalId
property.