@nodatachat/guard 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +28 -0
- package/README.md +120 -0
- package/dist/activation.d.ts +8 -0
- package/dist/activation.js +110 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +458 -0
- package/dist/code-scanner.d.ts +14 -0
- package/dist/code-scanner.js +309 -0
- package/dist/db-scanner.d.ts +7 -0
- package/dist/db-scanner.js +185 -0
- package/dist/fixers/fix-csrf.d.ts +9 -0
- package/dist/fixers/fix-csrf.js +113 -0
- package/dist/fixers/fix-gitignore.d.ts +9 -0
- package/dist/fixers/fix-gitignore.js +71 -0
- package/dist/fixers/fix-headers.d.ts +9 -0
- package/dist/fixers/fix-headers.js +118 -0
- package/dist/fixers/fix-pii-encrypt.d.ts +9 -0
- package/dist/fixers/fix-pii-encrypt.js +298 -0
- package/dist/fixers/fix-rate-limit.d.ts +9 -0
- package/dist/fixers/fix-rate-limit.js +102 -0
- package/dist/fixers/fix-rls.d.ts +9 -0
- package/dist/fixers/fix-rls.js +243 -0
- package/dist/fixers/fix-routes-auth.d.ts +9 -0
- package/dist/fixers/fix-routes-auth.js +82 -0
- package/dist/fixers/fix-secrets.d.ts +9 -0
- package/dist/fixers/fix-secrets.js +132 -0
- package/dist/fixers/index.d.ts +11 -0
- package/dist/fixers/index.js +37 -0
- package/dist/fixers/registry.d.ts +25 -0
- package/dist/fixers/registry.js +249 -0
- package/dist/fixers/scheduler.d.ts +9 -0
- package/dist/fixers/scheduler.js +254 -0
- package/dist/fixers/types.d.ts +160 -0
- package/dist/fixers/types.js +11 -0
- package/dist/reporter.d.ts +28 -0
- package/dist/reporter.js +185 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.js +5 -0
- package/package.json +61 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FullReport, MetadataReport, PIIFieldResult, RouteResult, SecretResult, RLSResult, InfraResult } from "./types";
|
|
2
|
+
interface ReportInput {
|
|
3
|
+
scanId: string;
|
|
4
|
+
projectName: string;
|
|
5
|
+
projectHash: string;
|
|
6
|
+
scanType: "code" | "db" | "full";
|
|
7
|
+
scanDurationMs: number;
|
|
8
|
+
previousScore: number | null;
|
|
9
|
+
code?: {
|
|
10
|
+
filesScanned: number;
|
|
11
|
+
framework: string;
|
|
12
|
+
database: string;
|
|
13
|
+
piiFields: PIIFieldResult[];
|
|
14
|
+
routes: RouteResult[];
|
|
15
|
+
secrets: SecretResult[];
|
|
16
|
+
};
|
|
17
|
+
db?: {
|
|
18
|
+
tables: number;
|
|
19
|
+
piiFields: PIIFieldResult[];
|
|
20
|
+
rls: RLSResult[];
|
|
21
|
+
infra: InfraResult;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function generateReports(input: ReportInput): {
|
|
25
|
+
full: FullReport;
|
|
26
|
+
metadata: MetadataReport;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ═══════════════════════════════════════════════════════════
|
|
3
|
+
// @nodatachat/guard — Dual Report Generator
|
|
4
|
+
//
|
|
5
|
+
// Generates TWO reports from the same scan:
|
|
6
|
+
//
|
|
7
|
+
// 1. full-report.json — STAYS LOCAL — customer sees everything
|
|
8
|
+
// Contains: actual field names, values context, full details
|
|
9
|
+
//
|
|
10
|
+
// 2. metadata-only.json — SENT TO NODATA — no data values
|
|
11
|
+
// Contains: table.column names, encrypted yes/no, counts
|
|
12
|
+
//
|
|
13
|
+
// The customer can diff the two files to see exactly what was redacted.
|
|
14
|
+
// ═══════════════════════════════════════════════════════════
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.generateReports = generateReports;
|
|
17
|
+
const crypto_1 = require("crypto");
|
|
18
|
+
const GUARD_VERSION = "1.0.0";
|
|
19
|
+
function generateReports(input) {
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
// Merge PII fields from code + DB
|
|
22
|
+
const allPII = [];
|
|
23
|
+
const seenFields = new Set();
|
|
24
|
+
if (input.code) {
|
|
25
|
+
for (const f of input.code.piiFields) {
|
|
26
|
+
const key = `${f.table}.${f.column}`;
|
|
27
|
+
if (!seenFields.has(key)) {
|
|
28
|
+
seenFields.add(key);
|
|
29
|
+
allPII.push(f);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (input.db) {
|
|
34
|
+
for (const f of input.db.piiFields) {
|
|
35
|
+
const key = `${f.table}.${f.column}`;
|
|
36
|
+
if (seenFields.has(key)) {
|
|
37
|
+
// DB result overrides code result (DB is ground truth)
|
|
38
|
+
const idx = allPII.findIndex(p => `${p.table}.${p.column}` === key);
|
|
39
|
+
if (idx >= 0)
|
|
40
|
+
allPII[idx] = { ...allPII[idx], ...f, encrypted: f.encrypted || allPII[idx].encrypted };
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
seenFields.add(key);
|
|
44
|
+
allPII.push(f);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Calculate scores
|
|
49
|
+
const totalPII = allPII.length;
|
|
50
|
+
const encryptedPII = allPII.filter(f => f.encrypted).length;
|
|
51
|
+
const plaintextPII = totalPII - encryptedPII;
|
|
52
|
+
const encCoverage = totalPII > 0 ? Math.round((encryptedPII / totalPII) * 100) : 100;
|
|
53
|
+
const totalRoutes = input.code?.routes.length || 0;
|
|
54
|
+
const protectedRoutes = input.code?.routes.filter(r => r.has_auth).length || 0;
|
|
55
|
+
const routeProtection = totalRoutes > 0 ? Math.round((protectedRoutes / totalRoutes) * 100) : 100;
|
|
56
|
+
const criticalSecrets = input.code?.secrets.filter(s => s.severity === "critical" && !s.is_env_interpolated).length || 0;
|
|
57
|
+
const highSecrets = input.code?.secrets.filter(s => s.severity === "high" && !s.is_env_interpolated).length || 0;
|
|
58
|
+
const medSecrets = input.code?.secrets.filter(s => s.severity === "medium" && !s.is_env_interpolated).length || 0;
|
|
59
|
+
// Overall score: weighted average
|
|
60
|
+
const encWeight = 0.5; // encryption is most important
|
|
61
|
+
const routeWeight = 0.25;
|
|
62
|
+
const secretWeight = 0.25;
|
|
63
|
+
const secretScore = Math.max(0, 100 - (criticalSecrets * 25 + highSecrets * 10 + medSecrets * 3));
|
|
64
|
+
const overallScore = Math.round(encCoverage * encWeight + routeProtection * routeWeight + secretScore * secretWeight);
|
|
65
|
+
const improved = input.previousScore !== null && overallScore > input.previousScore;
|
|
66
|
+
// Proof hash
|
|
67
|
+
const proofData = allPII.map(f => `${f.table}.${f.column}:${f.encrypted}`).join("|") + `|${overallScore}|${now}`;
|
|
68
|
+
const proofHash = (0, crypto_1.createHash)("sha256").update(proofData).digest("hex");
|
|
69
|
+
// ── Build metadata report (what we send) ──
|
|
70
|
+
const metadata = {
|
|
71
|
+
version: "1.0",
|
|
72
|
+
scan_id: input.scanId,
|
|
73
|
+
generated_at: now,
|
|
74
|
+
project_hash: input.projectHash,
|
|
75
|
+
scan_type: input.scanType,
|
|
76
|
+
scan_duration_ms: input.scanDurationMs,
|
|
77
|
+
guard_version: GUARD_VERSION,
|
|
78
|
+
scores: {
|
|
79
|
+
overall: overallScore,
|
|
80
|
+
previous: input.previousScore,
|
|
81
|
+
improved,
|
|
82
|
+
delta: input.previousScore !== null ? overallScore - input.previousScore : 0,
|
|
83
|
+
},
|
|
84
|
+
code_summary: input.code ? {
|
|
85
|
+
files_scanned: input.code.filesScanned,
|
|
86
|
+
framework: input.code.framework,
|
|
87
|
+
database: input.code.database,
|
|
88
|
+
pii_fields_total: input.code.piiFields.length,
|
|
89
|
+
pii_fields_encrypted: input.code.piiFields.filter(f => f.encrypted).length,
|
|
90
|
+
encryption_coverage_percent: encCoverage,
|
|
91
|
+
routes_total: totalRoutes,
|
|
92
|
+
routes_protected: protectedRoutes,
|
|
93
|
+
route_protection_percent: routeProtection,
|
|
94
|
+
secrets_found: (input.code.secrets.filter(s => !s.is_env_interpolated)).length,
|
|
95
|
+
secrets_critical: criticalSecrets,
|
|
96
|
+
// Per-field summary — NO VALUES, just table.column + status
|
|
97
|
+
fields: input.code.piiFields.map(f => ({
|
|
98
|
+
table: f.table,
|
|
99
|
+
column: f.column,
|
|
100
|
+
pii_type: f.pii_type,
|
|
101
|
+
encrypted: f.encrypted,
|
|
102
|
+
pattern: f.encryption_pattern,
|
|
103
|
+
})),
|
|
104
|
+
} : null,
|
|
105
|
+
db_summary: input.db ? {
|
|
106
|
+
tables: input.db.tables,
|
|
107
|
+
pii_fields_total: input.db.piiFields.length,
|
|
108
|
+
pii_fields_encrypted: input.db.piiFields.filter(f => f.encrypted).length,
|
|
109
|
+
encryption_coverage_percent: input.db.piiFields.length > 0
|
|
110
|
+
? Math.round((input.db.piiFields.filter(f => f.encrypted).length / input.db.piiFields.length) * 100)
|
|
111
|
+
: 100,
|
|
112
|
+
rls_tables_enabled: input.db.rls.filter(r => r.rls_enabled).length,
|
|
113
|
+
rls_tables_total: input.db.rls.length,
|
|
114
|
+
rls_coverage_percent: input.db.rls.length > 0
|
|
115
|
+
? Math.round((input.db.rls.filter(r => r.rls_enabled).length / input.db.rls.length) * 100)
|
|
116
|
+
: 0,
|
|
117
|
+
db_version: input.db.infra.db_version,
|
|
118
|
+
ssl: input.db.infra.ssl,
|
|
119
|
+
has_encrypt_functions: input.db.infra.encrypt_functions,
|
|
120
|
+
trigger_count: input.db.infra.trigger_count,
|
|
121
|
+
} : null,
|
|
122
|
+
issues: {
|
|
123
|
+
critical: criticalSecrets,
|
|
124
|
+
high: highSecrets + plaintextPII,
|
|
125
|
+
medium: medSecrets,
|
|
126
|
+
fixes_available: plaintextPII, // each plaintext field has a migration fix
|
|
127
|
+
},
|
|
128
|
+
proof_hash: proofHash,
|
|
129
|
+
privacy: {
|
|
130
|
+
contains_data_values: false,
|
|
131
|
+
contains_connection_strings: false,
|
|
132
|
+
contains_credentials: false,
|
|
133
|
+
contains_source_code: false,
|
|
134
|
+
contains_file_contents: false,
|
|
135
|
+
customer_can_verify: true,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
// ── Build full report (stays local) ──
|
|
139
|
+
const full = {
|
|
140
|
+
version: "1.0",
|
|
141
|
+
scan_id: input.scanId,
|
|
142
|
+
generated_at: now,
|
|
143
|
+
project_name: input.projectName,
|
|
144
|
+
scan_type: input.scanType,
|
|
145
|
+
scan_duration_ms: input.scanDurationMs,
|
|
146
|
+
overall_score: overallScore,
|
|
147
|
+
previous_score: input.previousScore,
|
|
148
|
+
improved,
|
|
149
|
+
code: input.code ? {
|
|
150
|
+
files_scanned: input.code.filesScanned,
|
|
151
|
+
framework: input.code.framework,
|
|
152
|
+
database: input.code.database,
|
|
153
|
+
pii_fields: input.code.piiFields,
|
|
154
|
+
routes: input.code.routes,
|
|
155
|
+
secrets: input.code.secrets,
|
|
156
|
+
encryption_coverage_percent: encCoverage,
|
|
157
|
+
route_protection_percent: routeProtection,
|
|
158
|
+
} : null,
|
|
159
|
+
db: input.db ? {
|
|
160
|
+
tables: input.db.tables,
|
|
161
|
+
pii_fields: input.db.piiFields,
|
|
162
|
+
rls: input.db.rls,
|
|
163
|
+
infra: input.db.infra,
|
|
164
|
+
encryption_coverage_percent: input.db.piiFields.length > 0
|
|
165
|
+
? Math.round((input.db.piiFields.filter(f => f.encrypted).length / input.db.piiFields.length) * 100)
|
|
166
|
+
: 100,
|
|
167
|
+
rls_coverage_percent: input.db.rls.length > 0
|
|
168
|
+
? Math.round((input.db.rls.filter(r => r.rls_enabled).length / input.db.rls.length) * 100)
|
|
169
|
+
: 0,
|
|
170
|
+
} : null,
|
|
171
|
+
summary: {
|
|
172
|
+
total_pii_fields: totalPII,
|
|
173
|
+
encrypted_fields: encryptedPII,
|
|
174
|
+
plaintext_fields: plaintextPII,
|
|
175
|
+
coverage_percent: encCoverage,
|
|
176
|
+
critical_issues: criticalSecrets,
|
|
177
|
+
high_issues: highSecrets + plaintextPII,
|
|
178
|
+
medium_issues: medSecrets,
|
|
179
|
+
fixes_available: plaintextPII,
|
|
180
|
+
},
|
|
181
|
+
proof_hash: proofHash,
|
|
182
|
+
metadata_preview: metadata,
|
|
183
|
+
};
|
|
184
|
+
return { full, metadata };
|
|
185
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export interface ActivationRequest {
|
|
2
|
+
license_key: string;
|
|
3
|
+
project_hash: string;
|
|
4
|
+
guard_version: string;
|
|
5
|
+
scan_type: "code" | "db" | "full";
|
|
6
|
+
}
|
|
7
|
+
export interface ActivationResponse {
|
|
8
|
+
success: boolean;
|
|
9
|
+
activation_key: string;
|
|
10
|
+
tier: string;
|
|
11
|
+
features: string[];
|
|
12
|
+
scan_id: string;
|
|
13
|
+
expires_at: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface PIIFieldResult {
|
|
17
|
+
table: string;
|
|
18
|
+
column: string;
|
|
19
|
+
pii_type: string;
|
|
20
|
+
encrypted: boolean;
|
|
21
|
+
encryption_pattern: string | null;
|
|
22
|
+
has_companion_column: boolean;
|
|
23
|
+
row_count: number;
|
|
24
|
+
encrypted_count: number;
|
|
25
|
+
}
|
|
26
|
+
export interface RouteResult {
|
|
27
|
+
path: string;
|
|
28
|
+
has_auth: boolean;
|
|
29
|
+
auth_type: string | null;
|
|
30
|
+
}
|
|
31
|
+
export interface SecretResult {
|
|
32
|
+
file: string;
|
|
33
|
+
line: number;
|
|
34
|
+
type: string;
|
|
35
|
+
severity: "critical" | "high" | "medium";
|
|
36
|
+
is_env_interpolated: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface RLSResult {
|
|
39
|
+
table: string;
|
|
40
|
+
rls_enabled: boolean;
|
|
41
|
+
policy_count: number;
|
|
42
|
+
}
|
|
43
|
+
export interface InfraResult {
|
|
44
|
+
db_type: string;
|
|
45
|
+
db_version: string;
|
|
46
|
+
ssl: boolean;
|
|
47
|
+
has_pgcrypto: boolean;
|
|
48
|
+
encrypt_functions: boolean;
|
|
49
|
+
trigger_count: number;
|
|
50
|
+
}
|
|
51
|
+
export interface FullReport {
|
|
52
|
+
version: "1.0";
|
|
53
|
+
scan_id: string;
|
|
54
|
+
generated_at: string;
|
|
55
|
+
project_name: string;
|
|
56
|
+
scan_type: "code" | "db" | "full";
|
|
57
|
+
scan_duration_ms: number;
|
|
58
|
+
overall_score: number;
|
|
59
|
+
previous_score: number | null;
|
|
60
|
+
improved: boolean;
|
|
61
|
+
code: {
|
|
62
|
+
files_scanned: number;
|
|
63
|
+
framework: string;
|
|
64
|
+
database: string;
|
|
65
|
+
pii_fields: PIIFieldResult[];
|
|
66
|
+
routes: RouteResult[];
|
|
67
|
+
secrets: SecretResult[];
|
|
68
|
+
encryption_coverage_percent: number;
|
|
69
|
+
route_protection_percent: number;
|
|
70
|
+
} | null;
|
|
71
|
+
db: {
|
|
72
|
+
tables: number;
|
|
73
|
+
pii_fields: PIIFieldResult[];
|
|
74
|
+
rls: RLSResult[];
|
|
75
|
+
infra: InfraResult;
|
|
76
|
+
encryption_coverage_percent: number;
|
|
77
|
+
rls_coverage_percent: number;
|
|
78
|
+
} | null;
|
|
79
|
+
summary: {
|
|
80
|
+
total_pii_fields: number;
|
|
81
|
+
encrypted_fields: number;
|
|
82
|
+
plaintext_fields: number;
|
|
83
|
+
coverage_percent: number;
|
|
84
|
+
critical_issues: number;
|
|
85
|
+
high_issues: number;
|
|
86
|
+
medium_issues: number;
|
|
87
|
+
fixes_available: number;
|
|
88
|
+
};
|
|
89
|
+
proof_hash: string;
|
|
90
|
+
metadata_preview: MetadataReport;
|
|
91
|
+
}
|
|
92
|
+
export interface MetadataReport {
|
|
93
|
+
version: "1.0";
|
|
94
|
+
scan_id: string;
|
|
95
|
+
generated_at: string;
|
|
96
|
+
project_hash: string;
|
|
97
|
+
scan_type: "code" | "db" | "full";
|
|
98
|
+
scan_duration_ms: number;
|
|
99
|
+
guard_version: string;
|
|
100
|
+
scores: {
|
|
101
|
+
overall: number;
|
|
102
|
+
previous: number | null;
|
|
103
|
+
improved: boolean;
|
|
104
|
+
delta: number;
|
|
105
|
+
};
|
|
106
|
+
code_summary: {
|
|
107
|
+
files_scanned: number;
|
|
108
|
+
framework: string;
|
|
109
|
+
database: string;
|
|
110
|
+
pii_fields_total: number;
|
|
111
|
+
pii_fields_encrypted: number;
|
|
112
|
+
encryption_coverage_percent: number;
|
|
113
|
+
routes_total: number;
|
|
114
|
+
routes_protected: number;
|
|
115
|
+
route_protection_percent: number;
|
|
116
|
+
secrets_found: number;
|
|
117
|
+
secrets_critical: number;
|
|
118
|
+
fields: Array<{
|
|
119
|
+
table: string;
|
|
120
|
+
column: string;
|
|
121
|
+
pii_type: string;
|
|
122
|
+
encrypted: boolean;
|
|
123
|
+
pattern: string | null;
|
|
124
|
+
}>;
|
|
125
|
+
} | null;
|
|
126
|
+
db_summary: {
|
|
127
|
+
tables: number;
|
|
128
|
+
pii_fields_total: number;
|
|
129
|
+
pii_fields_encrypted: number;
|
|
130
|
+
encryption_coverage_percent: number;
|
|
131
|
+
rls_tables_enabled: number;
|
|
132
|
+
rls_tables_total: number;
|
|
133
|
+
rls_coverage_percent: number;
|
|
134
|
+
db_version: string;
|
|
135
|
+
ssl: boolean;
|
|
136
|
+
has_encrypt_functions: boolean;
|
|
137
|
+
trigger_count: number;
|
|
138
|
+
} | null;
|
|
139
|
+
issues: {
|
|
140
|
+
critical: number;
|
|
141
|
+
high: number;
|
|
142
|
+
medium: number;
|
|
143
|
+
fixes_available: number;
|
|
144
|
+
};
|
|
145
|
+
proof_hash: string;
|
|
146
|
+
privacy: {
|
|
147
|
+
contains_data_values: false;
|
|
148
|
+
contains_connection_strings: false;
|
|
149
|
+
contains_credentials: false;
|
|
150
|
+
contains_source_code: false;
|
|
151
|
+
contains_file_contents: false;
|
|
152
|
+
customer_can_verify: true;
|
|
153
|
+
};
|
|
154
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nodatachat/guard",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "NoData Guard — continuous security scanner. Runs locally, reports only metadata. Your data never leaves your machine.",
|
|
5
|
+
"main": "./dist/cli.js",
|
|
6
|
+
"types": "./dist/cli.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nodata-guard": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "npx tsx src/cli.ts",
|
|
18
|
+
"scan": "npx tsx src/cli.ts --license-key NDC-DEV --skip-send",
|
|
19
|
+
"scan:plan": "npx tsx src/cli.ts --license-key NDC-DEV --skip-send --fix-plan",
|
|
20
|
+
"scan:fix": "npx tsx src/cli.ts --license-key NDC-DEV --skip-send --fix",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"start": "node dist/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"pg": "^8.13.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typescript": "^5.7.0",
|
|
32
|
+
"@types/pg": "^8.11.0",
|
|
33
|
+
"@types/node": "^22.0.0"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"security",
|
|
37
|
+
"soc2",
|
|
38
|
+
"soc1",
|
|
39
|
+
"compliance",
|
|
40
|
+
"encryption",
|
|
41
|
+
"audit",
|
|
42
|
+
"pii",
|
|
43
|
+
"scanner",
|
|
44
|
+
"guard",
|
|
45
|
+
"nodata",
|
|
46
|
+
"privacy",
|
|
47
|
+
"gdpr",
|
|
48
|
+
"cicd",
|
|
49
|
+
"devsecops"
|
|
50
|
+
],
|
|
51
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
52
|
+
"homepage": "https://nodatachat.com/guard",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/nodatachat/guard"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/nodatachat/guard/issues"
|
|
59
|
+
},
|
|
60
|
+
"author": "NoDataChat <support@nodatachat.com>"
|
|
61
|
+
}
|