Commit 3afa9b11 authored by Stefan Schott's avatar Stefan Schott
Browse files

Initial commit

parents
TEMPLATE_DIR=templates
MODULE_DIR=modules
OUTPUT_DIR=output
PROJECT_NAME=com.generated.app
ANDROID_SDK_DIR=path/to/Android/SDK
JDK_DIR=path/to/Java/jdk
\ No newline at end of file
node_modules
generated/.gradle
generated/app/build
generated/app/src/main/java
generated/app/src/main/AndroidManifest.xml
generated/app/src/main/res/layout/*
.gradle
output
const { cleanup, writeFileWithContent, copyFile, hashFile } = require('./helpers/FileHelper');
const { create } = require('xmlbuilder2');
const { resolve } = require('path');
class FileGenerator {
manifestContent = '';
layoutContent = '';
classesToGenerate = [];
config = '';
constructor (manifest, layout, classes, config) {
this.manifestContent = manifest;
this.layoutContent = layout;
this.classes = classes;
this.config = config;
}
generateSourceFiles() {
const mainPath = 'generated/app/src/main';
const projectName = process.env.PROJECT_NAME.split('.');
// remove previously generated files
this._cleanupDirectories(mainPath);
// generate AndroidManifest.xml
writeFileWithContent(`${mainPath}/AndroidManifest.xml`, this.manifestContent);
// generate activitiy.xml layout file
writeFileWithContent(`${mainPath}/res/layout/activity.xml`, this.layoutContent);
// generate all necessary java files containing classes
this.classes.forEach(elem => {
writeFileWithContent(`${mainPath}/java/${projectName.join('/')}/${elem.className}.java`, elem.classContent);
});
}
async finishCompilation(taintFlows, fullFlows) {
const date = new Date();
const outputDir = process.env.OUTPUT_DIR;
const finalOutputDir = `${outputDir}/${date.getFullYear()}_${date.getMonth()+1}_${date.getDate()}_${date.getTime()}`;
const apkFileName = `${finalOutputDir}/generated-app.apk`;
// copy compiled APK file to the specified output directory
copyFile('generated/app/build/outputs/apk/debug/app-debug.apk', apkFileName);
// generate config description
writeFileWithContent(`${finalOutputDir}/app-config.txt`, this.config);
// generate ground-truth
const groundTruthContent = await this._generateGroundTruthContent(taintFlows, apkFileName);
writeFileWithContent(`${finalOutputDir}/ground-truth.xml`, groundTruthContent);
// generate full flow ground-truth
const fullGroundTruthContent = await this._generateGroundTruthContent(fullFlows, apkFileName);
writeFileWithContent(`${finalOutputDir}/full-ground-truth.xml`, fullGroundTruthContent);
console.log('\x1b[33m%s\x1b[0m', 'Benchmark case has been successfully generated');
}
writeConfig() {
writeFileWithContent('generated/local.properties', `sdk.dir=${process.env.ANDROID_SDK_DIR}`);
}
_cleanupDirectories(path) {
cleanup(`${path}/AndroidManifest.xml`);
cleanup(`${path}/res/layout`);
cleanup(`${path}/java`);
}
async _generateGroundTruthContent(taintFlows, apkFilePath) {
const projectName = process.env.PROJECT_NAME;
const resolvedApkFilePath = resolve(apkFilePath).replace(/\\/g, '/');
const md5 = await hashFile(apkFilePath, 'md5');
const sha1 = await hashFile(apkFilePath, 'sha1');
const sha256 = await hashFile(apkFilePath, 'sha256');
const groundTruth = {
answer: {
flows: {
flow: []
}
}
};
const appField = {
file: resolvedApkFilePath,
hashes: {
hash: [
{
'@type': 'MD5',
'#': md5
},
{
'@type': 'SHA-1',
'#': sha1
},
{
'@type': 'SHA-256',
'#': sha256
}
]
}
}
const flow = taintFlows.map(taintFlow => (
{
reference: [
{
'@type': 'from',
statement: {
statementfull: "",
statementgeneric: taintFlow.from.statementSignature
},
method: `<${projectName}.${taintFlow.from.className}: ${taintFlow.from.methodSignature}>`,
classname: `${projectName}.${taintFlow.from.className}`,
app: appField
},
{
'@type': 'to',
statement: {
statementfull: "",
statementgeneric: taintFlow.to.statementSignature
},
method: `<${projectName}.${taintFlow.to.className}: ${taintFlow.to.methodSignature}>`,
classname: `${projectName}.${taintFlow.to.className}`,
app: appField
}
],
attributes: {
attribute: [
{
name: 'leaking',
value: taintFlow.to.leaking
},
{
name: 'reachable',
value: taintFlow.to.reachable
}
]
}
}
));
groundTruth.answer.flows.flow = flow;
const groundTruthDoc = create({ encoding: 'UTF-8', standalone: 'yes' }, groundTruth);
const groundTruthXml = groundTruthDoc.end({ prettyPrint: true });
return groundTruthXml;
}
log() {
console.log('\x1b[33m%s\x1b[0m', '--------- ANDROID MANIFEST --------');
console.log(this.manifestContent);
console.log('\x1b[33m%s\x1b[0m', '--------- LAYOUT --------');
console.log(this.layoutContent);
console.log('\x1b[33m%s\x1b[0m', '--------- SOURCE CODE --------');
this.classes.forEach(elem => {
console.log('\x1b[33m%s\x1b[0m', elem.className);
console.log(elem.classContent);
});
}
}
module.exports = FileGenerator;
\ No newline at end of file
const { escapeRegExp } = require('./helpers/RegexHelper');
const { concatIfNotNull } = require('./helpers/ArrayHelper');
class Preprocessor {
identifierCount = 0;
identifierLookup = {};
multiFlowIdentifierModules = 0;
multiFlowIdentifierMethods = 0;
multiFlowIdentifierGlobals = 0;
moduleCount = 0;
templateClassName = '';
templateMethodSignature = '';
addedData = { imports: [], permissions: [] };
moduleIdentifiers = [];
moduleTree = null;
constructor(template) {
this.templateClassName = template.className;
this.templateMethodSignature = template.methodSignature;
}
preprocessModule(module, parentId, childId, id) {
let processedModule = {
...module,
parentId,
childId,
id,
imports: this._joinCodeSnippet(this._filterAlreadyContained(module.imports, 'imports')),
globals: this._joinCodeSnippet(module.globals),
module: this._joinCodeSnippet(module.module),
methods: this._joinCodeSnippet(module.methods),
classes: this._joinCodeSnippet(module.classes),
permissions: this._joinCodeSnippet(this._filterAlreadyContained(module.permissions, 'permissions')),
components: this._joinCodeSnippet(module.components),
views: this._joinCodeSnippet(module.views),
project: process.env.PROJECT_NAME
};
// give identifiers numbers after the name so that there are no multiple identifier usages
processedModule = this._handleIdentifiers(processedModule);
// insert a placeholder into the module if the specific field is empty
processedModule = this._handleEmptyPlaceholders(processedModule);
// insert a new placeholder so that the one in the template is not consumed
processedModule = this._handleImportPlaceholder(processedModule);
// processedModule = this._handleGlobalPlaceholder(processedModule);
processedModule = this._handleManifestAndLayoutPlaceholders(processedModule);
// change module field name and placeholder so that the modules are inserted in the correct order and
// no module is inserted multiple times because there are multiple empty {{ module }} placeholders
// inside the template
processedModule = this._handleModuleInsertionPlaceholder(processedModule);
// retain {{ module }} placeholders that have not been inserted yet
processedModule = this._retainPlaceholders(processedModule);
for (let i = 0; i < this.multiFlowIdentifierModules; i++) {
this.moduleIdentifiers.push(parseInt(`${processedModule.id}${i}`));
}
this.multiFlowIdentifierModules = 0;
this.multiFlowIdentifierMethods = 0;
this.multiFlowIdentifierGlobals = 0;
this.moduleCount++;
return processedModule;
}
_joinCodeSnippet(codeSnippetArray) {
if (codeSnippetArray) {
return codeSnippetArray.join('\n');
}
return '';
}
_handleIdentifiers(module) {
const refinedModule = { ...module };
const identifierRegex = /(?<=\§).*?(?=\$)/g;
const identifiers = concatIfNotNull(
refinedModule.globals.match(identifierRegex),
refinedModule.module.match(identifierRegex),
refinedModule.methods.match(identifierRegex),
refinedModule.classes.match(identifierRegex),
refinedModule.components.match(identifierRegex),
refinedModule.views.match(identifierRegex)
);
// only get unique identifiers
const uniqueIdentifiers = [... new Set(identifiers)];
uniqueIdentifiers.forEach(identifier => {
const newIdentifier = identifier + this.identifierCount;
const replacer = new RegExp(escapeRegExp(${identifier}$`), 'g');
refinedModule.globals = refinedModule.globals.replace(replacer, newIdentifier);
refinedModule.module = refinedModule.module.replace(replacer, newIdentifier);
refinedModule.methods = refinedModule.methods.replace(replacer, newIdentifier);
refinedModule.classes = refinedModule.classes.replace(replacer, newIdentifier);
refinedModule.components = refinedModule.components.replace(replacer, newIdentifier);
refinedModule.views = refinedModule.views.replace(replacer, newIdentifier);
// handle identifiers in flow
refinedModule.flows.forEach((flow, idx) => {
refinedModule.flows[idx].className = flow.className.replace(replacer, newIdentifier);
refinedModule.flows[idx].methodSignature = flow.methodSignature.replace(replacer, newIdentifier);
});
this.identifierCount++;
});
return refinedModule;
}
_handleImportPlaceholder(module) {
const refinedModule = { ...module };
// check if data flow changed to different class
if (!/{{\s*imports\s*}}/.test(refinedModule.classes)) {
// if not
refinedModule.imports += '{{ imports }}';
}
return refinedModule;
}
_handleGlobalPlaceholder(module) {
const refinedModule = { ...module };
// check if data flow changed to different class
if (!/{{\s*globals\s*}}/.test(refinedModule.classes)) {
// if not
refinedModule.globals += '{{ globals }}';
}
return refinedModule;
}
_handleManifestAndLayoutPlaceholders(module) {
const refinedModule = { ...module };
// check if there is still a permissions placeholder
if (!/{{\s*permissions\s*}}/.test(refinedModule.permissions)) {
refinedModule.permissions += '\n{{ permissions }}';
}
// check if there is still a components placeholder
if (!/{{\s*components\s*}}/.test(refinedModule.components)) {
refinedModule.components += '\n{{ components }}';
}
// check if there is still a views placeholder
if (!/{{\s*views\s*}}/.test(refinedModule.views)) {
refinedModule.views += '\n{{ views }}';
}
return refinedModule;
}
_handleEmptyPlaceholders(module) {
const refinedModule = { ...module };
if (!refinedModule.permissions) {
refinedModule.permissions += '{{ permissions }}';
}
if (!refinedModule.components) {
refinedModule.components += '{{ components }}';
}
if (!refinedModule.views) {
refinedModule.views += '{{ views }}';
}
return refinedModule;
}
_handleModuleInsertionPlaceholder(module) {
const refinedModule = { ...module };
const modulePlaceholderRegex = /{{\s*module\s*}}/g;
const modulePlaceholderRegexSingle = /{{\s*module\s*}}/;
const methodsPlaceholderRegex = /{{\s*methods\s*}}/g;
const methodsPlaceholderRegexSingle = /{{\s*methods\s*}}/;
const globalsPlaceholderRegex = /{{\s*globals\s*}}/g;
const globalsPlaceholderRegexSingle = /{{\s*globals\s*}}/;
const modulePlaceholders = concatIfNotNull(
refinedModule.module.match(modulePlaceholderRegex),
refinedModule.methods.match(modulePlaceholderRegex),
refinedModule.classes.match(modulePlaceholderRegex)
);
const methodsPlaceholders = concatIfNotNull(
refinedModule.module.match(methodsPlaceholderRegex),
refinedModule.methods.match(methodsPlaceholderRegex),
refinedModule.classes.match(methodsPlaceholderRegex)
);
const globalsPlaceholders = concatIfNotNull(
refinedModule.module.match(globalsPlaceholderRegex),
refinedModule.methods.match(globalsPlaceholderRegex),
refinedModule.classes.match(globalsPlaceholderRegex),
refinedModule.globals.match(globalsPlaceholderRegex)
);
modulePlaceholders.forEach(_ => {
refinedModule.module = refinedModule.module.replace(modulePlaceholderRegexSingle, `{{ module${module.id}${this.multiFlowIdentifierModules} }}`);
refinedModule.methods = refinedModule.methods.replace(modulePlaceholderRegexSingle, `{{ module${module.id}${this.multiFlowIdentifierModules} }}`);
refinedModule.classes = refinedModule.classes.replace(modulePlaceholderRegexSingle, `{{ module${module.id}${this.multiFlowIdentifierModules} }}`);
this.multiFlowIdentifierModules++;
});
methodsPlaceholders.forEach(_ => {
refinedModule.module = refinedModule.module.replace(methodsPlaceholderRegexSingle, `{{ methods${module.id}${this.multiFlowIdentifierMethods} }}`);
refinedModule.methods = refinedModule.methods.replace(methodsPlaceholderRegexSingle, `{{ methods${module.id}${this.multiFlowIdentifierMethods} }}`);
refinedModule.classes = refinedModule.classes.replace(methodsPlaceholderRegexSingle, `{{ methods${module.id}${this.multiFlowIdentifierMethods} }}`);
this.multiFlowIdentifierMethods++;
});
globalsPlaceholders.forEach(_ => {
refinedModule.module = refinedModule.module.replace(globalsPlaceholderRegexSingle, `{{ globals${module.id}${this.multiFlowIdentifierGlobals} }}`);
refinedModule.methods = refinedModule.methods.replace(globalsPlaceholderRegexSingle, `{{ globals${module.id}${this.multiFlowIdentifierGlobals} }}`);
refinedModule.classes = refinedModule.classes.replace(globalsPlaceholderRegexSingle, `{{ globals${module.id}${this.multiFlowIdentifierGlobals} }}`);
refinedModule.globals = refinedModule.globals.replace(globalsPlaceholderRegexSingle, `{{ globals${module.id}${this.multiFlowIdentifierGlobals} }}`);
this.multiFlowIdentifierGlobals++;
});
if (this.moduleCount > 0) {
const moduleValue = refinedModule.module;
delete refinedModule['module'];
refinedModule[`module${module.parentId}${module.childId}`] = moduleValue;
const methodsValue = refinedModule.methods;
delete refinedModule['methods'];
refinedModule[`methods${module.parentId}${module.childId}`] = methodsValue;
const globalsValue = refinedModule.globals;
delete refinedModule['globals'];
refinedModule[`globals${module.parentId}${module.childId}`] = globalsValue;
}
return refinedModule;
}
_retainPlaceholders(module) {
const refinedModule = { ...module };
if (!refinedModule.module) {
refinedModule.module = '{{ module }}';
}
if (!refinedModule.methods) {
refinedModule.methods = '{{ methods }}';
}
if (!refinedModule.globals) {
refinedModule.globals = '{{ globals }}';
}
this.moduleIdentifiers.forEach(i => {
if (!refinedModule[`module${i}`]) {
refinedModule[`module${i}`] = `{{ module${i} }}`;
}
});
this.moduleIdentifiers.forEach(i => {
if (!refinedModule[`methods${i}`]) {
refinedModule[`methods${i}`] = `{{ methods${i} }}`;
}
});
this.moduleIdentifiers.forEach(i => {
if (!refinedModule[`globals${i}`]) {
refinedModule[`globals${i}`] = `{{ globals${i} }}`;
}
});
return refinedModule;
}
_filterAlreadyContained(moduleData, key) {
let filteredModuleData = [];
if (moduleData instanceof Array) {
moduleData.forEach(element => {
if (this.addedData[key] && !this.addedData[key].includes(element)) {
this.addedData[key].push(element);
filteredModuleData.push(element);
}
});
}
return filteredModuleData;
}
// Ground-Truth methods
preprocessFlows(ref, leaking = true, reachable = true, className=this.templateClassName, methodSignature=this.templateMethodSignature) {
let currentLeaking = leaking;
let currentReachable = reachable;
if (!!ref && ref.module !== 'empty') {
ref.flows?.forEach(flow => {
if (!flow.className) {
flow.className = className;
}
if (!flow.methodSignature) {
flow.methodSignature = methodSignature;
}
if (reachable && !flow.reachable) {
currentReachable = false;
}
if (leaking && !flow.leaking) {
currentLeaking = false;
}
if (currentReachable && ref.type.toLowerCase() === 'source') {
currentLeaking = flow.leaking;
}
flow.reachable = currentReachable;
flow.leaking = currentLeaking;
});
}
ref.children.forEach((child, idx) => {
if (child.module !== 'empty') {
this.preprocessFlows(child, ref.flows[idx].leaking, ref.flows[idx].reachable, ref.flows[idx].className, ref.flows[idx].methodSignature);
}
});
}
getSourceSinkConnections(ref, connections = [], source = '', idx = 0) {
let currentSource = source;
if (!!ref) {
if (ref.type && ref.type.toLowerCase() === 'source') {
currentSource = ref.flows[idx];
} else if (ref.type && ref.type.toLowerCase() === 'sink' && currentSource) {
// changed idx to 0
connections.push({ from: currentSource, to: ref.flows[0] });
}
ref.children.forEach((child, idx) => {
this.getSourceSinkConnections(child, connections, currentSource, idx);
});
}
return connections;
}
getAllConnections(ref, connections = [], parentFlow = null) {
if (!!ref) {
if (parentFlow && ref.flows && ref.flows.length > 0) {
ref.flows.forEach(flow => {
if (!flow.statementSignature) {
flow.statementSignature = parentFlow.statementSignature;
}
if (flow.statementSignature !== parentFlow.statementSignature) {
connections.push({ from: parentFlow, to: flow });
}
});
}
ref.children.forEach((child, idx) => {
this.getAllConnections(child, connections, ref.flows[idx]);
});
}
return connections;
}
}
module.exports = Preprocessor;
\ No newline at end of file
const hb = require('handlebars');
const formatJS = require('js-beautify').js;
const formatXML = require('xml-formatter');
class TemplateEngine {
projectName = process.env.PROJECT_NAME.split('.');
templateString;
manifestTemplateString;
layoutTemplateString;
classes = [];
constructor(template) {
this.templateString = template.template.join('\n');
this.layoutTemplateString = template.layout.join('\n');
const manifestTemplate = template.manifest.join('\n');
const manifestTemplateScript = hb.compile(manifestTemplate, { noEscape: true });
const initialModule = {
project: `"${this.projectName.join('.')}"`,
permissions: '{{ permissions }}',
components: '{{ components }}'
};
this.manifestTemplateString = manifestTemplateScript(initialModule);
}
insertModule(module) {
const templateScript = hb.compile(this.templateString, { noEscape: true });
this.templateString = templateScript(module);
const manifestTemplateScript = hb.compile(this.manifestTemplateString, { noEscape: true });
this.manifestTemplateString = manifestTemplateScript(module);
const layoutTemplateScript = hb.compile(this.layoutTemplateString, { noEscape: true });
this.layoutTemplateString = layoutTemplateScript(module);
}
cleanTemplate() {
this.templateString = this.templateString.replace(/{{.*}}/g, '');
this.manifestTemplateString = this.manifestTemplateString.replace(/{{.*}}/g, '');
this.layoutTemplateString = this.layoutTemplateString.replace(/{{.*}}/g, '');
}
beautifyTemplate() {
this.templateString = formatJS(this.templateString, {
indent_size: 2,
space_in_empty_paren: true,
max_preserve_newlines: 2
});
th