Skip to content

Commit 2fd5255

Browse files
committed
feat: merge the Chek for EXIF data
2 parents f6e347d + 7240f33 commit 2fd5255

File tree

12 files changed

+211
-6
lines changed

12 files changed

+211
-6
lines changed

package-lock.json

Lines changed: 29 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"express-http-proxy": "^2.0.0",
5353
"express-rate-limit": "^7.1.5",
5454
"express-session": "^1.17.1",
55+
"fs": "^0.0.1-security",
5556
"history": "5.3.0",
5657
"jsonschema": "^1.4.1",
5758
"load-plugin": "^6.0.0",
@@ -64,6 +65,7 @@
6465
"passport": "^0.7.0",
6566
"passport-activedirectory": "^1.0.4",
6667
"passport-local": "^1.0.0",
68+
"path": "^0.12.7",
6769
"perfect-scrollbar": "^1.5.5",
6870
"prop-types": "15.8.1",
6971
"react": "^16.13.1",

proxy.config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,11 @@
8383
"enabled": true,
8484
"blockPatterns": ["modelWeights", "largeDatasets", "aiLibraries", "configKeys", "aiFunctions"]
8585
}
86+
87+
8688
}
8789
}
90+
8891
},
8992
"attestationConfig": {
9093
"questions": [

src/proxy/chain.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const pushActionChain = [
1313
proc.push.checkSensitiveData, // checkSensitiveData added
1414
proc.push.getDiff,
1515
proc.push.checkForAiMlUsage,
16+
proc.push.checkExifJpeg,
1617
proc.push.clearBareClone,
1718
proc.push.scanDiff,
1819
proc.push.blockForAuth,
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const { ExifTool } = require('exiftool-vendored');
2+
const { Step } = require('../../actions');
3+
const config = require('../../../config');
4+
5+
const commitConfig = config.getCommitConfig();
6+
const validExtensions = ['.jpeg', '.png', '.jpg', '.tiff'];
7+
// Make sure you have modified the proxy.config.json;
8+
// Function to check sensitive EXIF data
9+
const checkSensitiveExifData = (metadata) => {
10+
let allSafe = true;
11+
12+
13+
14+
if (metadata.GPSLatitude || metadata.GPSLongitude) {
15+
console.log('GPS data detected; push is blocked due to sensitive EXIF metadata');
16+
allSafe = false;
17+
}
18+
19+
20+
if (metadata.Make || metadata.Model || metadata.Software) {
21+
console.log('Camera information detected; push is blocked due to sensitive EXIF metadata');
22+
allSafe = false;
23+
}
24+
25+
26+
return allSafe;
27+
};
28+
29+
// Function to retrieve EXIF data using ExifTool
30+
const getExifData = async (filePath) => {
31+
const exifTool = new ExifTool();
32+
try {
33+
const metadata = await exifTool.read(filePath);
34+
return metadata ? checkSensitiveExifData(metadata) : true;
35+
} catch (error) {
36+
console.log(`Error reading EXIF data from ${filePath}: ${error.message}`);
37+
return false;
38+
} finally {
39+
await exifTool.end();
40+
}
41+
};
42+
43+
// Helper function to parse file paths from git diff content
44+
const extractFilePathsFromDiff = (diffContent) => {
45+
const filePaths = [];
46+
const lines = diffContent.split('\n');
47+
48+
lines.forEach(line => {
49+
const match = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
50+
if (match) {
51+
filePaths.push(match[1]); // Extract the file path from "a/" in the diff line
52+
}
53+
});
54+
55+
return filePaths;
56+
};
57+
58+
// Main exec function
59+
const exec = async (req, action, log = console.log) => {
60+
61+
const diffStep = action.steps.find((s) => s.stepName === 'diff');
62+
const step = new Step('checkExifJpeg');
63+
const allowedFileType = commitConfig.diff.block.ProxyFileTypes;
64+
65+
if (diffStep && diffStep.content) {
66+
const filePaths = extractFilePathsFromDiff(diffStep.content);
67+
const filteredPaths = filePaths.filter(path => validExtensions.some(ext => path.endsWith(ext) && allowedFileType.includes(ext)));
68+
69+
if (filteredPaths.length > 0) {
70+
const exifResults = await Promise.all(filteredPaths.map(filePath => getExifData(filePath)));
71+
const isBlocked = exifResults.some(result => !result);
72+
73+
if (isBlocked) {
74+
step.blocked = true;
75+
step.error = true;
76+
step.errorMessage = 'Your push has been blocked due to sensitive EXIF metadata detection in an image';
77+
log(step.errorMessage);
78+
}
79+
} else {
80+
log('No valid image files found in the diff content.');
81+
}
82+
} else {
83+
log('No diff content available.');
84+
}
85+
86+
action.addStep(step);
87+
return action;
88+
};
89+
90+
exec.displayName = 'CheckExif.exec';
91+
module.exports = { exec };

src/proxy/processors/push-action/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ exports.checkUserPushPermission = require('./checkUserPushPermission').exec;
1313
exports.clearBareClone = require('./clearBareClone').exec;
1414
exports.checkSensitiveData = require('./checkSensitiveData').exec;
1515
exports.checkForAiMlusage = require('./checkForAiMlUsage').exec;
16+
exports.checkExifJpeg = require('./checkExifJpeg').exec;

test/CheckExif.test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const { exec } = require('../src/proxy/processors/push-action/checkExifJpeg.js');
2+
const sinon = require('sinon');
3+
const { Action } = require('../src/proxy/actions/Action.js');
4+
const { Step } = require('../src/proxy/actions/Step.js');
5+
6+
7+
describe('Check EXIF Data From Images', () => {
8+
let logStub;
9+
10+
11+
beforeEach(() => {
12+
// Stub console.log and config.getCommitConfig for isolation in each test case
13+
logStub = sinon.stub(console, 'log');
14+
15+
});
16+
17+
afterEach(() => {
18+
// Restore stubs to avoid cross-test interference
19+
logStub.restore();
20+
// configStub.restore();
21+
});
22+
23+
const createDiffContent = (filePaths) => {
24+
// Creates diff-like content for each file path to simulate actual git diff output
25+
return filePaths.map(filePath => `diff --git a/${filePath} b/${filePath}`).join('\n');
26+
};
27+
28+
it('Should block push when sensitive EXIF metadata is found (GPS)', async () => {
29+
30+
31+
// Create action and step instances with test data that should trigger blocking
32+
const action = new Action('action_id', 'push', 'create', Date.now(), 'owner/repo');
33+
const step = new Step('diff');
34+
35+
// Set content with sensitive EXIF metadata in the test image file
36+
step.setContent(createDiffContent(['test/test_data/jpg/Sensitive_EXIF.jpg']));
37+
action.addStep(step);
38+
39+
await exec(null, action);
40+
41+
// Check that console.log was called with the blocking message
42+
sinon.assert.calledWith(logStub, sinon.match(/Your push has been blocked due to sensitive EXIF metadata detection in an image/));
43+
});
44+
45+
it('Should block push when sensitive EXIF metadata is found (Camera Info)', async () => {
46+
47+
48+
const action = new Action('action_id', 'push', 'create', Date.now(), 'owner/repo');
49+
const step = new Step('diff');
50+
51+
// Set content for a file that contains Camera-Info EXIF data
52+
step.setContent(createDiffContent(['test/test_data/jpg/Sensitive_Data_EXIF_2.jpg']));
53+
action.addStep(step);
54+
55+
await exec(null, action);
56+
57+
// Assert blocking message was logged
58+
sinon.assert.calledWith(logStub, sinon.match(/Your push has been blocked due to sensitive EXIF metadata detection in an image/));
59+
});
60+
61+
it('Should allow push when no sensitive EXIF metadata is found', async () => {
62+
// Configure with no sensitive EXIF parameters
63+
64+
65+
const action = new Action('action_id', 'push', 'create', Date.now(), 'owner/repo');
66+
const step = new Step('diff');
67+
68+
// Set content for a non-sensitive EXIF file
69+
step.setContent(createDiffContent(['test/test_data/jpg/Not_Sensitive_EXIF.jpg']));
70+
action.addStep(step);
71+
72+
await exec(null, action);
73+
74+
// Ensure no blocking message was logged
75+
sinon.assert.neverCalledWith(logStub, sinon.match(/Your push has been blocked due to sensitive EXIF metadata detection in an image/));
76+
});
77+
78+
79+
});

test/chain.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const mockPushProcessors = {
2828
getDiff: sinon.stub(),
2929
checkSensitiveData : sinon.stub(),
3030
clearBareClone: sinon.stub(),
31+
checkExifJpeg : sinon.stub(),
3132
scanDiff: sinon.stub(),
3233
blockForAuth: sinon.stub(),
3334
};
@@ -41,7 +42,6 @@ mockPushProcessors.checkIfWaitingAuth.displayName = 'checkIfWaitingAuth';
4142
mockPushProcessors.pullRemote.displayName = 'pullRemote';
4243
mockPushProcessors.writePack.displayName = 'writePack';
4344
mockPushProcessors.getDiff.displayName = 'getDiff';
44-
mockPushProcessors.checkSensitiveData.displayName = 'checkSensitiveData';
4545
mockPushProcessors.clearBareClone.displayName = 'clearBareClone';
4646
mockPushProcessors.scanDiff.displayName = 'scanDiff';
4747
mockPushProcessors.blockForAuth.displayName = 'blockForAuth';
@@ -107,6 +107,7 @@ describe('proxy chain', function () {
107107
mockPushProcessors.parsePush.resolves(continuingAction);
108108
mockPushProcessors.checkRepoInAuthorisedList.resolves(continuingAction);
109109
mockPushProcessors.checkCommitMessages.resolves(continuingAction);
110+
mockPushProcessors.checkEXIFJpeg.resolves(continuingAction);
110111
mockPushProcessors.checkAuthorEmails.resolves(continuingAction);
111112
mockPushProcessors.checkUserPushPermission.resolves(continuingAction);
112113
mockPushProcessors.checkSensitiveData.resolves(continuingAction);
@@ -124,7 +125,6 @@ describe('proxy chain', function () {
124125
expect(mockPushProcessors.checkIfWaitingAuth.called).to.be.true;
125126
expect(mockPushProcessors.pullRemote.called).to.be.false;
126127
expect(mockPushProcessors.audit.called).to.be.true;
127-
expect(mockPushProcessors.checkSensitiveData.called).to.be.false;
128128

129129
expect(result.type).to.equal('push');
130130
expect(result.allowPush).to.be.false;
@@ -140,8 +140,8 @@ describe('proxy chain', function () {
140140
mockPushProcessors.checkCommitMessages.resolves(continuingAction);
141141
mockPushProcessors.checkAuthorEmails.resolves(continuingAction);
142142
mockPushProcessors.checkUserPushPermission.resolves(continuingAction);
143-
mockPushProcessors.checkSensitiveData.resolves(continuingAction);
144143
// this stops the chain from further execution
144+
145145
mockPushProcessors.checkIfWaitingAuth.resolves({ type: 'push', continue: () => true, allowPush: true });
146146
const result = await chain.executeChain(req);
147147

@@ -154,7 +154,6 @@ describe('proxy chain', function () {
154154
expect(mockPushProcessors.checkIfWaitingAuth.called).to.be.true;
155155
expect(mockPushProcessors.pullRemote.called).to.be.false;
156156
expect(mockPushProcessors.audit.called).to.be.true;
157-
expect(mockPushProcessors.checkSensitiveData.called).to.be.false;
158157

159158
expect(result.type).to.equal('push');
160159
expect(result.allowPush).to.be.true;
@@ -174,10 +173,10 @@ describe('proxy chain', function () {
174173
mockPushProcessors.pullRemote.resolves(continuingAction);
175174
mockPushProcessors.writePack.resolves(continuingAction);
176175
mockPushProcessors.getDiff.resolves(continuingAction);
176+
mockPushProcessors.checkEXIFJpeg.resolves(continuingAction);
177177
mockPushProcessors.clearBareClone.resolves(continuingAction);
178178
mockPushProcessors.scanDiff.resolves(continuingAction);
179179
mockPushProcessors.blockForAuth.resolves(continuingAction);
180-
mockPushProcessors.checkSensitiveData.resolves(continuingAction);
181180

182181
const result = await chain.executeChain(req);
183182

@@ -191,6 +190,7 @@ describe('proxy chain', function () {
191190
expect(mockPushProcessors.pullRemote.called).to.be.true;
192191
expect(mockPushProcessors.writePack.called).to.be.true;
193192
expect(mockPushProcessors.getDiff.called).to.be.true;
193+
expect(mockPushProcessors.checkEXIFJpeg.called).to.be.true;
194194
expect(mockPushProcessors.clearBareClone.called).to.be.true;
195195
expect(mockPushProcessors.scanDiff.called).to.be.true;
196196
expect(mockPushProcessors.blockForAuth.called).to.be.true;
416 KB
Loading
1.06 KB
Loading

0 commit comments

Comments
 (0)