Skip to content

Commit fe70163

Browse files
author
Corentin Mors
authored
Add human-readable option for dates in team (#146)
Add human-readable dates to team Members and Logs commands. Fix #143
1 parent 160f482 commit fe70163

File tree

9 files changed

+190
-21
lines changed

9 files changed

+190
-21
lines changed

documentation/pages/business/audit-logs.mdx

+19-3
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ date -d @1688629046919
7070
date -r 1688629046919
7171
```
7272

73-
## Export the logs as CSV
73+
## Options
74+
75+
### Export as CSV
7476

7577
You can export the logs as CSV using the `--csv` option.
7678

@@ -81,7 +83,21 @@ dcli t logs --csv --start 0 --end now > logs.csv
8183
This allows you to open the logs in a spreadsheet editor like Excel or Google Sheets.
8284
Note that the `properties` field is kept as a JSON string in the CSV file because its content varies depending on the log type.
8385

84-
## Logs types (default)
86+
### Human Readable dates
87+
88+
You can use the `--human-readable` option to output the logs with human readable dates.
89+
90+
```sh copy
91+
dcli t logs --human-readable
92+
```
93+
94+
The date will be displayed in the ISO 8601 format.
95+
96+
Note that a new key named `date_time_iso` will be added to the logs.
97+
98+
## Logs types
99+
100+
### Default types
85101

86102
| Type | Event message |
87103
| ---------------------------------------------- | ------------------------------------------------------ |
@@ -125,7 +141,7 @@ Note that the `properties` field is kept as a JSON string in the CSV file becaus
125141
| billing_admin_added | Made %(name)s the billing contact |
126142
| billing_admin_removed | Revoked %(name)s as the billing contact |
127143

128-
## Logs types (sensitive)
144+
### Sensitive types
129145

130146
You can turn on logging sensitive actions in the Policies section of Settings in the Admin Console. Read more about it in our [dedicated Help Center article](https://support.dashlane.com/hc/en-us/articles/4414606120210).
131147

documentation/pages/business/members.mdx

+15-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ You can pipe the output to `jq` to filter the results:
2020
dcli t members | jq '.members[] | select(.isTeamCaptain == true)'
2121
```
2222

23-
## Exporting the list as a CSV
23+
## Options
24+
25+
### Exporting the list as a CSV
2426

2527
You can use the `--csv` flag to export the list as a CSV file:
2628

@@ -30,6 +32,18 @@ dcli t members --csv > members.csv
3032

3133
This allows you to open the file in a spreadsheet editor such as Excel or Google Sheets.
3234

35+
### Human Readable dates
36+
37+
You can use the `--human-readable` option to output the logs with human readable dates.
38+
39+
```sh copy
40+
dcli t members --human-readable
41+
```
42+
43+
The dates will be displayed in the ISO 8601 format.
44+
45+
Note that keys ending with `Unix` will be converted to human readable dates and renamed to remove the `Unix` suffix.
46+
3347
## Members interface
3448

3549
| Property | Type | Description |

src/command-handlers/devices.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { connectAndPrepare, reset } from '../modules/database';
22
import { deactivateDevices, listDevices, ListDevicesOutput } from '../endpoints';
3-
import { askConfirmReset, unixTimestampToHumanReadable } from '../utils';
3+
import { askConfirmReset, epochTimestampToIso } from '../utils';
44

55
type OutputDevice = ListDevicesOutput['devices'][number] & {
66
isCurrentDevice: boolean;
@@ -29,7 +29,7 @@ export async function listAllDevices(options: { json: boolean }) {
2929
id: device.deviceId,
3030
name: device.deviceName,
3131
platform: device.devicePlatform,
32-
lastActivity: unixTimestampToHumanReadable(device.lastActivityDateUnix),
32+
lastActivity: epochTimestampToIso(device.lastActivityDateUnix),
3333
current: device.isCurrentDevice,
3434
};
3535
});

src/command-handlers/teamDevices.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { connectAndPrepare } from '../modules/database';
22
import { listTeamDevices } from '../endpoints';
3-
import { unixTimestampToHumanReadable } from '../utils';
3+
import { epochTimestampToIso } from '../utils';
44

55
export async function listAllTeamDevices(options: { json: boolean }) {
66
const { db, secrets } = await connectAndPrepare({ autoSync: false });
@@ -32,9 +32,9 @@ export async function listAllTeamDevices(options: { json: boolean }) {
3232
accessKey: device.accessKey,
3333
name: device.deviceName,
3434
platform: device.platform,
35-
creationDate: unixTimestampToHumanReadable(device.creationDateUnix),
36-
updateDate: unixTimestampToHumanReadable(device.updateDateUnix),
37-
lastActivityDate: unixTimestampToHumanReadable(device.lastActivityDateUnix),
35+
creationDate: epochTimestampToIso(device.creationDateUnix),
36+
updateDate: epochTimestampToIso(device.updateDateUnix),
37+
lastActivityDate: epochTimestampToIso(device.lastActivityDateUnix),
3838
};
3939
});
4040

src/command-handlers/teamLogs.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import winston from 'winston';
22
import { connectAndPrepare } from '../modules/database';
33
import { StartAuditLogsQueryParams, startAuditLogsQuery, getAuditLogQueryResults } from '../endpoints';
4-
import { getTeamDeviceCredentials, jsonToCsv } from '../utils';
4+
import { getTeamDeviceCredentials, jsonToCsv, epochTimestampToIso } from '../utils';
5+
import { GenericLog } from '../types/logs';
56

67
export const runTeamLogs = async (options: {
78
start: string;
89
end: string;
910
type: string;
1011
category: string;
1112
csv: boolean;
13+
humanReadable: boolean;
1214
}) => {
1315
const teamDeviceCredentials = getTeamDeviceCredentials();
1416

1517
const { start, type, category } = options;
1618
const end = options.end === 'now' ? Date.now().toString() : options.end;
1719

1820
const { db } = await connectAndPrepare({ autoSync: false });
19-
const logs = await getAuditLogs({
21+
let logs = await getAuditLogs({
2022
teamDeviceCredentials,
2123
startDateRangeUnix: parseInt(start),
2224
endDateRangeUnix: parseInt(end),
@@ -25,17 +27,26 @@ export const runTeamLogs = async (options: {
2527
});
2628
db.close();
2729

30+
if (options.humanReadable) {
31+
logs = logs.map((log) => {
32+
return {
33+
...log,
34+
date_time_iso: epochTimestampToIso(log.date_time, true),
35+
};
36+
});
37+
}
38+
2839
if (options.csv) {
29-
console.log(jsonToCsv(logs.map((log) => JSON.parse(log) as object)));
40+
console.log(jsonToCsv(logs));
3041
return;
3142
}
3243

33-
logs.forEach((log) => console.log(log));
44+
logs.forEach((log) => console.log(JSON.stringify(log)));
3445
};
3546

3647
const MAX_RESULT = 1000;
3748

38-
export const getAuditLogs = async (params: StartAuditLogsQueryParams): Promise<string[]> => {
49+
export const getAuditLogs = async (params: StartAuditLogsQueryParams): Promise<GenericLog[]> => {
3950
const { teamDeviceCredentials } = params;
4051

4152
const { queryExecutionId } = await startAuditLogsQuery(params);
@@ -65,5 +76,5 @@ export const getAuditLogs = async (params: StartAuditLogsQueryParams): Promise<s
6576
logs = logs.concat(result.results);
6677
}
6778

68-
return logs;
79+
return logs.map((log) => JSON.parse(log) as GenericLog);
6980
};

src/command-handlers/teamMembers.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { getTeamMembers as getTeamMembersRequest } from '../endpoints';
2-
import { getTeamDeviceCredentials, flattenJsonArrayOfObject, jsonToCsv } from '../utils';
2+
import { getTeamDeviceCredentials, flattenJsonArrayOfObject, jsonToCsv, epochTimestampToIso } from '../utils';
33

44
interface GetTeamMembersParams {
55
page: number;
66
limit: number;
77
csv: boolean;
8+
humanReadable: boolean;
89
}
910

1011
export const runTeamMembers = async (params: GetTeamMembersParams) => {
@@ -17,6 +18,25 @@ export const runTeamMembers = async (params: GetTeamMembersParams) => {
1718
limit,
1819
});
1920

21+
if (params.humanReadable) {
22+
response.members = response.members.map((member) => {
23+
const memberWithHumanReadableDates = {
24+
...member,
25+
joinedDate: epochTimestampToIso(member.joinedDateUnix),
26+
invitedDate: epochTimestampToIso(member.invitedDateUnix),
27+
revokedDate: epochTimestampToIso(member.revokedDateUnix),
28+
lastUpdateDate: epochTimestampToIso(member.lastUpdateDateUnix),
29+
};
30+
31+
delete memberWithHumanReadableDates.joinedDateUnix;
32+
delete memberWithHumanReadableDates.invitedDateUnix;
33+
delete memberWithHumanReadableDates.revokedDateUnix;
34+
delete memberWithHumanReadableDates.lastUpdateDateUnix;
35+
36+
return memberWithHumanReadableDates;
37+
});
38+
}
39+
2040
if (params.csv) {
2141
if (response.pages) {
2242
console.log(`Page ${response.page + 1} of ${response.pages}`);

src/commands/team/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ export const teamCommands = (params: { program: Command }) => {
3030
.argument('[page]', 'Page number', '0')
3131
.argument('[limit]', 'Limit of members per page', '0')
3232
.option('--csv', 'Output in CSV format')
33-
.action(async (page: string, limit: string, options: { csv: boolean }) => {
33+
.option('--human-readable', 'Output dates in human readable format')
34+
.action(async (page: string, limit: string, options: { csv: boolean; humanReadable: boolean }) => {
3435
await runTeamMembers({
3536
page: parseInt(page),
3637
limit: parseInt(limit),
3738
csv: options.csv,
39+
humanReadable: options.humanReadable,
3840
});
3941
});
4042

@@ -47,6 +49,7 @@ export const teamCommands = (params: { program: Command }) => {
4749
.option('--type <type>', 'log type')
4850
.option('--category <category>', 'log category')
4951
.option('--csv', 'Output in CSV format')
52+
.option('--human-readable', 'Output dates in human readable format')
5053
.action(runTeamLogs);
5154

5255
teamGroup

src/types/logs.ts

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
export interface GenericLog {
2+
schema_version: string;
3+
/**
4+
* The audit log's unique ID
5+
*/
6+
uuid: string;
7+
/**
8+
* The team ID the user that performed the action is a member of.
9+
*/
10+
team_id: number;
11+
/**
12+
* The user ID of the user that performed the action.
13+
*/
14+
author_user_id: number;
15+
/**
16+
* The user ID of the user that is the target of the action.
17+
*/
18+
target_user_id?: number;
19+
/**
20+
* The sharing group ID the performed action is associated with.
21+
*/
22+
sharing_group_id?: number;
23+
/**
24+
* The type of the audit log. (e.g. user_device_added).
25+
*/
26+
log_type: string;
27+
/**
28+
* A milliseconds timestamp of the date and time the action occurred.
29+
*/
30+
date_time: number;
31+
author_client?: {
32+
/**
33+
* Dashlane client version of the user that did or logged the action.
34+
*/
35+
version?: string | null;
36+
/**
37+
* Dashlane client platform of the user that did or logged the action.
38+
*/
39+
platform?: string | null;
40+
};
41+
/**
42+
* The category the audit log type falls under.
43+
*/
44+
category:
45+
| 'account'
46+
| 'authentication'
47+
| 'dark_web_monitoring'
48+
| 'groups'
49+
| 'import_export'
50+
| 'sharing'
51+
| 'team_settings'
52+
| 'team_settings_activedirectory'
53+
| 'team_settings_policies'
54+
| 'team_settings_samlprovisioning'
55+
| 'team_settings_scim'
56+
| 'team_settings_sso'
57+
| 'user_settings'
58+
| 'user_settings_accountrecovery'
59+
| 'users'
60+
| 'vault_ids'
61+
| 'vault_passwords'
62+
| 'vault_payments'
63+
| 'vault_personalinfo'
64+
| 'vault_securenotes';
65+
/**
66+
* The team device ID of the team that performed the action.
67+
*/
68+
author_team_device_id?: number;
69+
/**
70+
* Whether the audit log contains sensitive data.
71+
*/
72+
is_sensitive?: boolean;
73+
properties?: {
74+
[k: string]: any;
75+
};
76+
encrypted_properties?: {
77+
[k: string]: any;
78+
};
79+
server_encrypted_properties?: {
80+
[k: string]: any;
81+
};
82+
meta?: {
83+
/**
84+
* The encryption key used to encrypt the encrypted_properties property.
85+
*/
86+
encrypted_properties_encryption_key_uuid?: string;
87+
/**
88+
* The encryption key used to encrypt the server_encrypted_properties property.
89+
*/
90+
server_encrypted_properties_encryption_key_uuid?: string;
91+
};
92+
}

src/utils/strings.ts

+16-3
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,22 @@ export const removeUnderscoresAndCapitalize = (string: string): string => {
2222
.join(' ');
2323
};
2424

25-
/** Unix timestamp to human readable string */
26-
export const unixTimestampToHumanReadable = (timestamp: number | null): string => {
27-
return timestamp ? new Date(timestamp * 1000).toLocaleString() : 'N/A';
25+
/** Epoch unix timestamp in seconds to ISO 8601 */
26+
export const epochTimestampToIso = (
27+
timestamp: string | number | null | undefined,
28+
inMilliseconds?: boolean
29+
): string => {
30+
if (timestamp === null || timestamp === undefined) {
31+
return '';
32+
}
33+
34+
let timestampNumber = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
35+
36+
if (!inMilliseconds) {
37+
timestampNumber *= 1000;
38+
}
39+
40+
return new Date(timestampNumber).toISOString();
2841
};
2942

3043
export const jsonToCsv = (json: Record<string, any>): string => {

0 commit comments

Comments
 (0)