Skip to content

Commit 87084f6

Browse files
authored
fix: Use local file when importing csvs, and use clamd (LearningLocker#1141)
* fix: Use local file when importing csvs * ci(lint): Remove unused import * fix: Use clamdscan for faster scanning * Update .env.example * Update .env.example * fix: Handle clamscan failures greacefully * ci(lint): Linting fixes * fix: Show persona dates * ci(lint): Fix linting
1 parent 9c61e3d commit 87084f6

File tree

10 files changed

+104
-31
lines changed

10 files changed

+104
-31
lines changed

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,4 +193,5 @@ FS_REPO=local
193193
########
194194

195195
# Location of virus scanning binary (ClamAV - https://www.clamav.net/)
196-
#CLAMSCAN_BINARY=/usr/bin/clamscan
196+
#CLAMDSCAN_BINARY=/usr/bin/clamdscan
197+
#CLAMDSCAN_CONF=/etc/clamav/clamd.conf

lib/helpers/getDateFromMongoID.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import isString from 'lodash/isString';
2+
3+
export default (_id) => {
4+
if (!isString(_id) || _id.length !== 24) {
5+
// not a mongo id
6+
throw Error('Not a Mongo ID');
7+
}
8+
const timehex = _id.substring(0, 8);
9+
10+
// convert to a number... base 16
11+
const secondsSinceEpoch = parseInt(timehex, 16);
12+
if (isNaN(secondsSinceEpoch)) {
13+
throw Error('Invalid date in Mongo ID');
14+
}
15+
16+
// convert to milliseconds, and create a new date
17+
return new Date(secondsSinceEpoch * 1000);
18+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { expect } from 'chai';
2+
import getDateFromMongoID from '../getDateFromMongoID';
3+
4+
const _id = '5ad52af00000000000000000';
5+
describe('getDateFromMongoID', () => {
6+
it('should return date for Mongo ID as string', () => {
7+
const date = getDateFromMongoID(_id);
8+
const expectedDate = new Date('2018-04-16T23:00:00');
9+
expect(date.getTime()).to.equal(expectedDate.getTime());
10+
});
11+
12+
it('should error when _id is not a string', () => {
13+
expect(getDateFromMongoID.bind(null, 123)).to.throw(Error);
14+
expect(getDateFromMongoID.bind(null, false)).to.throw(Error);
15+
expect(getDateFromMongoID.bind(null, undefined)).to.throw(Error);
16+
expect(getDateFromMongoID.bind(null, null)).to.throw(Error);
17+
expect(getDateFromMongoID.bind(null, {})).to.throw(Error);
18+
expect(getDateFromMongoID.bind(null, [])).to.throw(Error);
19+
});
20+
21+
it('should error when _id is not 24 chars', () => {
22+
expect(getDateFromMongoID.bind(null, 'abc')).to.throw(Error);
23+
expect(getDateFromMongoID.bind(null, `${_id}0`)).to.throw(Error);
24+
});
25+
26+
it('should error when _id does not contain a valid hex date', () => {
27+
expect(getDateFromMongoID.bind(null, 'Zad52af00000000000000000')).to.throw(Error);
28+
});
29+
});

lib/models/statementForwarding.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,15 @@ const schema = new mongoose.Schema({
166166
query: {
167167
type: String,
168168
validate: {
169-
validator: value => {
169+
validator: (value) => {
170170
try {
171171
JSON.parse(value);
172172
} catch (err) {
173173
return false;
174174
}
175175
return true;
176176
},
177-
message: "Invalid query"
177+
message: 'Invalid query'
178178
}
179179
},
180180
isPublic: { type: Boolean, default: false }

lib/services/files/clamscan.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
11
import Promise from 'bluebird';
22
import logger from 'lib/logger';
3+
import defaultTo from 'lodash/defaultTo';
34

4-
const scanError = err => (
5-
new Error(`Something went wrong when scanning the file. ${err.message}`)
6-
);
75
const virusError = () => (
86
new Error('This file has not passed the virus scan and will be deleted.')
97
);
108
export default filePath => new Promise((resolve, reject) => {
11-
if (!process.env.CLAMSCAN_BINARY) {
12-
logger.warn('CLAMSCAN NOT INSTALLED, SEE DOCS FOR FURTHER INFORMATION.');
9+
if (!process.env.CLAMDSCAN_BINARY) {
10+
logger.warn('CLAMDSCAN NOT INSTALLED, SEE DOCS FOR FURTHER INFORMATION.');
1311
return resolve(filePath);
1412
}
1513
try {
1614
const clam = require('clamscan')({
1715
remove_infected: true,
18-
clamscan: {
19-
path: process.env.CLAMSCAN_BINARY,
16+
clamdscan: {
17+
path: process.env.CLAMDSCAN_BINARY,
18+
config_file: defaultTo(process.env.CLAMDSCAN_CONF, '/etc/clamav/clamd.conf')
2019
},
21-
preference: 'clamscan',
20+
preference: 'clamdscan',
2221
});
2322

2423
clam.is_infected(filePath, (err, file, isInfected) => {
25-
if (err) return reject(scanError(err));
24+
if (err) {
25+
logger.warn('ERROR SCANNING FILE WITH CLAMD - ', err);
26+
return resolve(filePath);
27+
}
2628
if (isInfected) return reject(virusError());
2729
return resolve(filePath);
2830
});
2931
} catch (err) {
30-
return reject(scanError(err));
32+
logger.warn('ERROR SCANNING FILE WITH CLAMD - ', err);
33+
return resolve(filePath);
3134
}
3235
});

lib/services/importPersonas/getCsvHeaders.js

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
1-
import * as fileService from 'lib/services/files';
21
import csv from 'fast-csv';
32
import { isUndefined, uniq } from 'lodash';
43
import EmptyCsvError from 'lib/errors/EmptyCsvError';
54
import DuplicateCsvHeadersError from 'lib/errors/DuplicateCsvHeadersError';
65

7-
const getCsvHeaders = async (handle) => {
8-
const csvStream = csv.parse({
6+
const getCsvHeaders = async (fileStream) => {
7+
const csvStreamHandler = csv.parse({
98
headers: false,
109
quoteHeaders: true
1110
});
1211

1312
// read the first row.
1413
let headers;
1514

16-
csvStream.on('data', (data) => {
15+
csvStreamHandler.on('data', (data) => {
1716
// we're just interested in the first one.
1817
if (isUndefined(headers)) headers = data;
1918
});
2019

2120
const csvPromise = new Promise((resolve, reject) => {
22-
csvStream.on('error', reject);
23-
csvStream.on('finish', resolve);
21+
csvStreamHandler.on('error', reject);
22+
csvStreamHandler.on('finish', resolve);
2423
});
2524

26-
fileService.downloadToStream(handle)(csvStream);
25+
fileStream.pipe(csvStreamHandler);
2726

2827
await csvPromise;
2928

lib/services/importPersonas/persistPersonas.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'fs';
22
import clamscan from 'lib/services/files/clamscan';
33
import { uploadFromStream } from 'lib/services/files/storage';
44

5-
export const PERSONAS_CSV_PATH = '/personasCsvs';
5+
export const PERSONAS_CSV_PATH = 'personasCsvs';
66

77
export default async ({
88
file,

lib/services/importPersonas/uploadPersonas.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import fs from 'fs';
12
import persistPersonas from 'lib/services/importPersonas/persistPersonas';
23
import PersonasImport from 'lib/models/personasImport';
34
import { STAGE_CONFIGURE_FIELDS } from 'lib/constants/personasImport';
@@ -29,7 +30,10 @@ const uploadPersonas = async ({
2930
id
3031
});
3132

32-
const csvHeaders = await getCsvHeaders(handle);
33+
// instead of going back out to s3, use the local file we already have!
34+
const filePath = file.path;
35+
const fileStream = fs.createReadStream(filePath);
36+
const csvHeaders = await getCsvHeaders(fileStream);
3337

3438
const structure = await getStructure({
3539
csvHeaders,
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import moment from 'moment';
22

3-
export default createdAt =>
4-
`Created ${moment(createdAt).fromNow()} - ${moment(createdAt).format('YYYY-MM-DD HH:mm:ss')}`;
3+
export default (createdAt) => {
4+
if (!createdAt) return '';
5+
return `Created ${moment(createdAt).fromNow()} - ${moment(createdAt).format('YYYY-MM-DD HH:mm:ss')}`;
6+
};

ui/src/containers/Owner/index.js

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
11
import React from 'react';
2+
import getDateFromMongoID from 'lib/helpers/getDateFromMongoID';
23
import { withProps, compose } from 'recompose';
34
import { withModel } from 'ui/utils/hocs';
45
import createdAtFormatter from './CreatedAt';
56

7+
const getCreatedDate = (createdAt, _id) => {
8+
if (!createdAt && _id) {
9+
try {
10+
return getDateFromMongoID(_id);
11+
} catch (err) {
12+
// error getting date from id (might not be a valid mongo id)
13+
return;
14+
}
15+
}
16+
return createdAt;
17+
};
18+
619
const Creator = compose(
720
withProps(({ model }) => ({ schema: 'user', id: model.get('owner') })),
821
withModel
9-
)(({ model, createdAt }) => {
22+
)(({ model, date }) => {
1023
const email = model.get('email', false);
1124
const name = model.get('name', false);
1225
return (
1326
<div>
1427
<div style={{ fontWeight: 'bold' }}>Made by { email ? <i>{name || email}</i> : <i>deleted user</i> }</div>
15-
<div>{ createdAtFormatter(createdAt) }</div>
28+
<div>{ date }</div>
1629
</div>
1730
);
1831
});
1932

20-
export default ({ model }) => (
21-
model.has('owner')
22-
? <Creator model={model} createdAt={model.get('createdAt', false)} />
23-
: <div style={{ marginTop: 8 }}>{ createdAtFormatter(model.get('createdAt')) }</div>
24-
);
33+
export default ({ model }) => {
34+
const date = getCreatedDate(model.get('createdAt', false), model.get('_id'));
35+
const formattedDate = createdAtFormatter(date);
36+
return (
37+
model.has('owner')
38+
? <Creator model={model} date={formattedDate} />
39+
: <div style={{ marginTop: 8 }}>{ formattedDate }</div>
40+
);
41+
};

0 commit comments

Comments
 (0)