@slashgear/gdpr-cookie-scanner 1.0.0 → 1.2.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.
@@ -1,8 +1,10 @@
1
1
  import { execFile } from "child_process";
2
- import { writeFile, mkdir } from "fs/promises";
3
- import { basename, dirname, join } from "path";
2
+ import { writeFile, mkdir, readFile } from "fs/promises";
3
+ import { basename, dirname, extname, join } from "path";
4
4
  import { promisify } from "util";
5
5
  import { fileURLToPath } from "url";
6
+ import { Marked } from "marked";
7
+ import { generatePdf } from "./pdf.js";
6
8
 
7
9
  const execFileAsync = promisify(execFile);
8
10
  const oxfmtBin = join(dirname(fileURLToPath(import.meta.url)), "../../node_modules/.bin/oxfmt");
@@ -18,7 +20,7 @@ import type { ScanOptions } from "../types.js";
18
20
  export class ReportGenerator {
19
21
  constructor(private readonly options: ScanOptions) {}
20
22
 
21
- async generate(result: ScanResult): Promise<string> {
23
+ async generate(result: ScanResult): Promise<{ reportPath: string; pdfPath: string }> {
22
24
  await mkdir(this.options.outputDir, { recursive: true });
23
25
 
24
26
  const hostname = new URL(result.url).hostname.replace(/^www\./, "");
@@ -36,7 +38,154 @@ export class ReportGenerator {
36
38
  await writeFile(checklistPath, checklist, "utf-8");
37
39
  await execFileAsync(oxfmtBin, [checklistPath]).catch(() => {});
38
40
 
39
- return outputPath;
41
+ const cookiesFilename = `gdpr-cookies-${hostname}-${date}.md`;
42
+ const cookiesPath = join(this.options.outputDir, cookiesFilename);
43
+ const cookiesInventory = this.buildCookiesInventory(result);
44
+ await writeFile(cookiesPath, cookiesInventory, "utf-8");
45
+ await execFileAsync(oxfmtBin, [cookiesPath]).catch(() => {});
46
+
47
+ const combined = [markdown, checklist, cookiesInventory].join("\n\n---\n\n");
48
+ const rawBody = await this.buildHtmlBody(combined);
49
+ const body = await this.inlineImages(rawBody, this.options.outputDir);
50
+ const html = this.wrapHtml(body, hostname);
51
+ const pdfFilename = `gdpr-report-${hostname}-${date}.pdf`;
52
+ const pdfPath = join(this.options.outputDir, pdfFilename);
53
+ await generatePdf(html, pdfPath);
54
+
55
+ return { reportPath: outputPath, pdfPath };
56
+ }
57
+
58
+ private async buildHtmlBody(markdown: string): Promise<string> {
59
+ type TocEntry = { level: number; text: string; id: string };
60
+ const entries: TocEntry[] = [];
61
+ const idCounts = new Map<string, number>();
62
+
63
+ const slugify = (text: string): string => {
64
+ const base =
65
+ text
66
+ .replace(/[^\p{L}\p{N}\s-]/gu, "")
67
+ .trim()
68
+ .toLowerCase()
69
+ .replace(/\s+/g, "-")
70
+ .replace(/-+/g, "-") || "section";
71
+ const count = idCounts.get(base) ?? 0;
72
+ idCounts.set(base, count + 1);
73
+ return count === 0 ? base : `${base}-${count}`;
74
+ };
75
+
76
+ const localMarked = new Marked();
77
+ localMarked.use({
78
+ renderer: {
79
+ heading({ text, depth }: { text: string; depth: number }) {
80
+ const id = slugify(text);
81
+ if (depth <= 2) entries.push({ level: depth, text, id });
82
+ return `<h${depth} id="${id}">${text}</h${depth}>\n`;
83
+ },
84
+ },
85
+ });
86
+
87
+ const body = await localMarked.parse(markdown);
88
+
89
+ if (entries.length === 0) return body;
90
+
91
+ const tocItems = entries
92
+ .map(({ level, text, id }) => {
93
+ const cls = level === 1 ? "toc-h1" : "toc-h2";
94
+ return `<li class="${cls}"><a href="#${id}">${text}</a></li>`;
95
+ })
96
+ .join("\n");
97
+
98
+ const toc = `<nav class="toc">
99
+ <p class="toc-title">Table of Contents</p>
100
+ <ul>
101
+ ${tocItems}
102
+ </ul>
103
+ </nav>`;
104
+
105
+ return toc + "\n" + body;
106
+ }
107
+
108
+ private async inlineImages(html: string, outputDir: string): Promise<string> {
109
+ const mimeTypes: Record<string, string> = {
110
+ ".png": "image/png",
111
+ ".jpg": "image/jpeg",
112
+ ".jpeg": "image/jpeg",
113
+ ".gif": "image/gif",
114
+ ".webp": "image/webp",
115
+ };
116
+
117
+ const imgRegex = /<img([^>]*)\ssrc="([^"#][^"]*)"([^>]*)>/gi;
118
+ const replacements: Array<{ original: string; replacement: string }> = [];
119
+
120
+ for (const match of html.matchAll(imgRegex)) {
121
+ const [full, before, src, after] = match;
122
+ if (src.startsWith("data:") || src.startsWith("http://") || src.startsWith("https://"))
123
+ continue;
124
+ const mime = mimeTypes[extname(src).toLowerCase()];
125
+ if (!mime) continue;
126
+ try {
127
+ const buf = await readFile(join(outputDir, src));
128
+ replacements.push({
129
+ original: full,
130
+ replacement: `<img${before} src="data:${mime};base64,${buf.toString("base64")}"${after}>`,
131
+ });
132
+ } catch {
133
+ // file not found — leave the tag as-is
134
+ }
135
+ }
136
+
137
+ return replacements.reduce(
138
+ (acc, { original, replacement }) => acc.replace(original, replacement),
139
+ html,
140
+ );
141
+ }
142
+
143
+ private wrapHtml(body: string, hostname: string): string {
144
+ return `<!DOCTYPE html>
145
+ <html lang="en">
146
+ <head>
147
+ <meta charset="UTF-8">
148
+ <title>GDPR Report — ${hostname}</title>
149
+ <style>
150
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
151
+ font-size: 11pt; line-height: 1.6; color: #1a1a1a; max-width: 900px;
152
+ margin: 0 auto; padding: 0 8px; }
153
+ h1 { font-size: 18pt; border-bottom: 2px solid #1a1a2e; padding-bottom: 6px;
154
+ color: #1a1a2e; margin-top: 2em; }
155
+ h2 { font-size: 14pt; color: #1a1a2e; margin-top: 1.5em; }
156
+ h3 { font-size: 12pt; margin-top: 1.2em; }
157
+ table { width: 100%; border-collapse: collapse; font-size: 9.5pt;
158
+ margin: 1em 0; page-break-inside: auto; }
159
+ th { background: #f0f0f4; padding: 6px 10px; text-align: left;
160
+ border-bottom: 2px solid #ccc; }
161
+ td { padding: 5px 10px; border-bottom: 1px solid #eee; vertical-align: top; }
162
+ tr { page-break-inside: avoid; }
163
+ code { font-family: "SFMono-Regular", Consolas, monospace; background: #f4f4f4;
164
+ padding: 1px 5px; border-radius: 3px; font-size: 9pt; }
165
+ pre { background: #f4f4f4; padding: 12px; border-radius: 4px;
166
+ overflow-x: auto; font-size: 9pt; }
167
+ blockquote { border-left: 3px solid #ccc; margin: 0.5em 0;
168
+ padding: 0.5em 1em; color: #555; }
169
+ hr { border: none; border-top: 1px solid #ddd; margin: 2em 0;
170
+ page-break-after: always; }
171
+ a { color: #0066cc; }
172
+ img { max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px; }
173
+ nav.toc { background: #f4f5f8; border-left: 4px solid #1a1a2e; border-radius: 4px;
174
+ padding: 14px 20px; margin: 0 0 2.5em 0; page-break-inside: avoid; }
175
+ .toc-title { font-weight: 700; font-size: 11pt; margin: 0 0 10px 0; color: #1a1a2e; }
176
+ nav.toc ul { list-style: none; margin: 0; padding: 0; }
177
+ nav.toc li { margin: 3px 0; line-height: 1.4; }
178
+ .toc-h1 { font-weight: 600; margin-top: 6px; }
179
+ .toc-h2 { padding-left: 1.2em; font-size: 9.5pt; }
180
+ nav.toc a { color: #0055aa; text-decoration: none; }
181
+ @media print {
182
+ h1 { page-break-before: always; }
183
+ h1:first-child { page-break-before: avoid; }
184
+ }
185
+ </style>
186
+ </head>
187
+ <body>${body}</body>
188
+ </html>`;
40
189
  }
41
190
 
42
191
  private buildMarkdown(r: ScanResult): string {
@@ -193,7 +342,7 @@ ${row("Cookie behavior", breakdown.cookieBehavior, 25)}
193
342
  const { modal } = r;
194
343
  const acceptBtn = modal.buttons.find((b) => b.type === "accept");
195
344
  const rejectBtn = modal.buttons.find((b) => b.type === "reject");
196
- const prefBtn = modal.buttons.find((b) => b.type === "preferences");
345
+ const _prefBtn = modal.buttons.find((b) => b.type === "preferences");
197
346
 
198
347
  const preTicked = modal.checkboxes.filter((c) => c.isCheckedByDefault);
199
348
 
@@ -460,6 +609,131 @@ ${rows.join("\n")}
460
609
  return recs.join("\n\n");
461
610
  }
462
611
 
612
+ private buildCookiesInventory(r: ScanResult): string {
613
+ const hostname = new URL(r.url).hostname;
614
+ const scanDate = new Date(r.scanDate).toLocaleString("en-GB");
615
+
616
+ // Collect all cookies across all phases, keyed by name+domain
617
+ type CookieEntry = {
618
+ name: string;
619
+ domain: string;
620
+ category: string;
621
+ phases: Set<string>;
622
+ expires: number | null;
623
+ httpOnly: boolean;
624
+ secure: boolean;
625
+ requiresConsent: boolean;
626
+ };
627
+
628
+ const cookieMap = new Map<string, CookieEntry>();
629
+
630
+ const phaseLabel: Record<ScannedCookie["capturedAt"], string> = {
631
+ "before-interaction": "before consent",
632
+ "after-accept": "after acceptance",
633
+ "after-reject": "after rejection",
634
+ };
635
+
636
+ const allCookies = [
637
+ ...r.cookiesBeforeInteraction,
638
+ ...r.cookiesAfterAccept,
639
+ ...r.cookiesAfterReject,
640
+ ];
641
+
642
+ for (const c of allCookies) {
643
+ const key = `${c.name}||${c.domain}`;
644
+ if (!cookieMap.has(key)) {
645
+ cookieMap.set(key, {
646
+ name: c.name,
647
+ domain: c.domain,
648
+ category: c.category,
649
+ phases: new Set(),
650
+ expires: c.expires,
651
+ httpOnly: c.httpOnly,
652
+ secure: c.secure,
653
+ requiresConsent: c.requiresConsent,
654
+ });
655
+ }
656
+ cookieMap.get(key)!.phases.add(phaseLabel[c.capturedAt]);
657
+ }
658
+
659
+ const expires = (entry: CookieEntry): string => {
660
+ if (entry.expires === null) return "Session";
661
+ const days = Math.round((entry.expires * 1000 - Date.now()) / 86400000);
662
+ if (days < 0) return "Expired";
663
+ if (days === 0) return "< 1 day";
664
+ if (days < 30) return `${days} days`;
665
+ return `${Math.round(days / 30)} months`;
666
+ };
667
+
668
+ const categoryLabel: Record<string, string> = {
669
+ "strictly-necessary": "Strictly necessary",
670
+ analytics: "Analytics",
671
+ advertising: "Advertising",
672
+ social: "Social",
673
+ personalization: "Personalization",
674
+ unknown: "Unknown",
675
+ };
676
+
677
+ const entries = [...cookieMap.values()].sort((a, b) => {
678
+ // Sort: strictly-necessary first, then by category, then by name
679
+ const order = [
680
+ "strictly-necessary",
681
+ "analytics",
682
+ "advertising",
683
+ "social",
684
+ "personalization",
685
+ "unknown",
686
+ ];
687
+ const oa = order.indexOf(a.category);
688
+ const ob = order.indexOf(b.category);
689
+ if (oa !== ob) return oa - ob;
690
+ return a.name.localeCompare(b.name);
691
+ });
692
+
693
+ const lines: string[] = [];
694
+
695
+ lines.push(`# Cookie Inventory — ${hostname}`);
696
+ lines.push(`
697
+ > **Scan date:** ${scanDate}
698
+ > **Scanned URL:** ${r.url}
699
+ > **Unique cookies detected:** ${entries.length}
700
+ `);
701
+
702
+ lines.push(`## Instructions`);
703
+ lines.push(`
704
+ This table lists all cookies detected during the scan, across all phases.
705
+ The **Description / Purpose** column is to be filled in by the DPO or technical owner.
706
+
707
+ - **Before consent** — cookie present from page load, before any interaction
708
+ - **After acceptance** — cookie set or persisting after clicking "Accept all"
709
+ - **After rejection** — cookie present after clicking "Reject all"
710
+ `);
711
+
712
+ lines.push(`## Cookie table\n`);
713
+ lines.push(
714
+ `| Cookie | Domain | Category | Phases | Expiry | Consent required | Description / Purpose |`,
715
+ );
716
+ lines.push(
717
+ `|--------|--------|----------|--------|--------|------------------|-----------------------|`,
718
+ );
719
+
720
+ for (const entry of entries) {
721
+ const phases = [...entry.phases].join(", ");
722
+ const consent = entry.requiresConsent ? "⚠️ Yes" : "✅ No";
723
+ const cat = categoryLabel[entry.category] ?? entry.category;
724
+ lines.push(
725
+ `| \`${entry.name}\` | ${entry.domain} | ${cat} | ${phases} | ${expires(entry)} | ${consent} | <!-- fill in --> |`,
726
+ );
727
+ }
728
+
729
+ lines.push(`\n---`);
730
+ lines.push(
731
+ `\n_Automatically generated by gdpr-cookie-scanner. Categories marked "Unknown" could not be identified automatically and should be verified manually._\n`,
732
+ );
733
+
734
+ return lines.join("\n") + "\n";
735
+ }
736
+
463
737
  private buildChecklist(r: ScanResult): string {
464
738
  const hostname = new URL(r.url).hostname;
465
739
  const scanDate = new Date(r.scanDate).toLocaleString("en-GB");
@@ -485,7 +759,8 @@ ${rows.join("\n")}
485
759
  rows.push({
486
760
  category: "Consent",
487
761
  rule: "Consent modal detected",
488
- reference: "RGPD Art. 7 · Dir. ePrivacy Art. 5(3)",
762
+ reference:
763
+ "[GDPR Art. 7](https://gdpr-info.eu/art-7-gdpr/) · [ePrivacy Dir. Art. 5(3)](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32002L0058)",
489
764
  status: r.modal.detected ? ok : ko,
490
765
  detail: r.modal.detected
491
766
  ? `Detected (\`${r.modal.selector}\`)`
@@ -496,7 +771,7 @@ ${rows.join("\n")}
496
771
  rows.push({
497
772
  category: "Consent",
498
773
  rule: "No pre-ticked checkboxes",
499
- reference: "RGPD Recital 32",
774
+ reference: "[GDPR Recital 32](https://gdpr-info.eu/recitals/no-32/)",
500
775
  status: preTicked.length === 0 ? ok : ko,
501
776
  detail:
502
777
  preTicked.length === 0
@@ -509,7 +784,7 @@ ${rows.join("\n")}
509
784
  rows.push({
510
785
  category: "Consent",
511
786
  rule: "Accept button label is unambiguous",
512
- reference: "RGPD Art. 4(11)",
787
+ reference: "[GDPR Art. 4(11)](https://gdpr-info.eu/art-4-gdpr/)",
513
788
  status:
514
789
  !r.modal.detected || !misleadingAccept
515
790
  ? ok
@@ -531,7 +806,8 @@ ${rows.join("\n")}
531
806
  rows.push({
532
807
  category: "Easy refusal",
533
808
  rule: "Reject button present at first layer",
534
- reference: "CNIL Recommendation 2022",
809
+ reference:
810
+ "[CNIL Recommendation 2022](https://www.cnil.fr/fr/cookies-et-autres-traceurs/regles/cookies/recommandation-sur-les-cookies-et-autres-traceurs)",
535
811
  status: !r.modal.detected ? ko : noReject ? ko : ok,
536
812
  detail: !r.modal.detected
537
813
  ? "Modal not detected"
@@ -544,7 +820,8 @@ ${rows.join("\n")}
544
820
  rows.push({
545
821
  category: "Easy refusal",
546
822
  rule: "Rejecting requires no more clicks than accepting",
547
- reference: "CNIL Recommendation 2022",
823
+ reference:
824
+ "[CNIL Recommendation 2022](https://www.cnil.fr/fr/cookies-et-autres-traceurs/regles/cookies/recommandation-sur-les-cookies-et-autres-traceurs)",
548
825
  status: !r.modal.detected ? ko : clickIssue ? ko : ok,
549
826
  detail: !r.modal.detected
550
827
  ? "Modal not detected"
@@ -559,7 +836,8 @@ ${rows.join("\n")}
559
836
  rows.push({
560
837
  category: "Easy refusal",
561
838
  rule: "Size symmetry between Accept and Reject",
562
- reference: "CEPD Guidelines 03/2022",
839
+ reference:
840
+ "[EDPB Guidelines 03/2022](https://edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-032022-dark-patterns-social-media-platform_en)",
563
841
  status: !r.modal.detected ? ko : sizeIssue ? warn : ok,
564
842
  detail: !r.modal.detected
565
843
  ? "Modal not detected"
@@ -572,7 +850,8 @@ ${rows.join("\n")}
572
850
  rows.push({
573
851
  category: "Easy refusal",
574
852
  rule: "Font symmetry between Accept and Reject",
575
- reference: "CEPD Guidelines 03/2022",
853
+ reference:
854
+ "[EDPB Guidelines 03/2022](https://edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-032022-dark-patterns-social-media-platform_en)",
576
855
  status: !r.modal.detected ? ko : nudgeIssue ? warn : ok,
577
856
  detail: !r.modal.detected
578
857
  ? "Modal not detected"
@@ -585,7 +864,8 @@ ${rows.join("\n")}
585
864
  rows.push({
586
865
  category: "Transparency",
587
866
  rule: "Granular controls available",
588
- reference: "CEPD Guidelines 05/2020",
867
+ reference:
868
+ "[EDPB Guidelines 05/2020](https://edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-052020-consent-under-regulation-2016679_en)",
589
869
  status: !r.modal.detected ? ko : r.modal.hasGranularControls ? ok : warn,
590
870
  detail: !r.modal.detected
591
871
  ? "Modal not detected"
@@ -595,21 +875,25 @@ ${rows.join("\n")}
595
875
  });
596
876
 
597
877
  const infoChecks: Array<{ key: string; label: string; ref: string }> = [
598
- { key: "purposes", label: "Processing purposes mentioned", ref: "RGPD Art. 13-14" },
878
+ {
879
+ key: "purposes",
880
+ label: "Processing purposes mentioned",
881
+ ref: "[GDPR Art. 13-14](https://gdpr-info.eu/art-13-gdpr/)",
882
+ },
599
883
  {
600
884
  key: "third-parties",
601
885
  label: "Sub-processors / third parties mentioned",
602
- ref: "RGPD Art. 13-14",
886
+ ref: "[GDPR Art. 13-14](https://gdpr-info.eu/art-13-gdpr/)",
603
887
  },
604
888
  {
605
889
  key: "duration",
606
890
  label: "Retention period mentioned",
607
- ref: "RGPD Art. 13(2)(a)",
891
+ ref: "[GDPR Art. 13(2)(a)](https://gdpr-info.eu/art-13-gdpr/)",
608
892
  },
609
893
  {
610
894
  key: "withdrawal",
611
895
  label: "Right to withdraw consent mentioned",
612
- ref: "RGPD Art. 7(3)",
896
+ ref: "[GDPR Art. 7(3)](https://gdpr-info.eu/art-7-gdpr/)",
613
897
  },
614
898
  ];
615
899
 
@@ -635,7 +919,8 @@ ${rows.join("\n")}
635
919
  rows.push({
636
920
  category: "Cookie behavior",
637
921
  rule: "No non-essential cookie before consent",
638
- reference: "RGPD Art. 7 · Dir. ePrivacy Art. 5(3)",
922
+ reference:
923
+ "[GDPR Art. 7](https://gdpr-info.eu/art-7-gdpr/) · [ePrivacy Dir. Art. 5(3)](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32002L0058)",
639
924
  status: illegalPre.length === 0 ? ok : ko,
640
925
  detail:
641
926
  illegalPre.length === 0
@@ -649,7 +934,8 @@ ${rows.join("\n")}
649
934
  rows.push({
650
935
  category: "Cookie behavior",
651
936
  rule: "Non-essential cookies removed after rejection",
652
- reference: "RGPD Art. 7 · CNIL Recommendation 2022",
937
+ reference:
938
+ "[GDPR Art. 7](https://gdpr-info.eu/art-7-gdpr/) · [CNIL Recommendation 2022](https://www.cnil.fr/fr/cookies-et-autres-traceurs/regles/cookies/recommandation-sur-les-cookies-et-autres-traceurs)",
653
939
  status: persistAfterReject.length === 0 ? ok : ko,
654
940
  detail:
655
941
  persistAfterReject.length === 0
@@ -663,7 +949,8 @@ ${rows.join("\n")}
663
949
  rows.push({
664
950
  category: "Cookie behavior",
665
951
  rule: "No network tracker before consent",
666
- reference: "RGPD Art. 7 · Dir. ePrivacy Art. 5(3)",
952
+ reference:
953
+ "[GDPR Art. 7](https://gdpr-info.eu/art-7-gdpr/) · [ePrivacy Dir. Art. 5(3)](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32002L0058)",
667
954
  status: preTrackers.length === 0 ? ok : ko,
668
955
  detail:
669
956
  preTrackers.length === 0
@@ -0,0 +1,21 @@
1
+ import { writeFile } from "fs/promises";
2
+ import { chromium } from "playwright";
3
+
4
+ export async function generatePdf(html: string, outputPath: string): Promise<void> {
5
+ const browser = await chromium.launch({
6
+ headless: true,
7
+ args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"],
8
+ });
9
+ try {
10
+ const page = await browser.newPage();
11
+ await page.setContent(html, { waitUntil: "domcontentloaded" });
12
+ const pdf = await page.pdf({
13
+ format: "A4",
14
+ printBackground: true,
15
+ margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
16
+ });
17
+ await writeFile(outputPath, pdf);
18
+ } finally {
19
+ await browser.close();
20
+ }
21
+ }
@@ -97,7 +97,7 @@ export class Scanner {
97
97
  await clearState(session2.context);
98
98
  const interceptor4 = createNetworkInterceptor(session2.page, "after-accept");
99
99
 
100
- let cookiesAfterAccept = cookiesBeforeInteraction;
100
+ let cookiesAfterAccept: typeof cookiesBeforeInteraction = [];
101
101
  let networkAfterAccept: typeof networkBeforeInteraction = [];
102
102
 
103
103
  try {
@@ -105,8 +105,14 @@ export class Scanner {
105
105
  waitUntil: "networkidle",
106
106
  timeout: this.options.timeout,
107
107
  });
108
- await session2.page.waitForTimeout(2000);
108
+ } catch (err) {
109
+ errors.push(`Accept phase navigation timeout: ${String(err)}`);
110
+ }
109
111
 
112
+ // Give a moment for late-loading scripts even if networkidle timed out
113
+ await session2.page.waitForTimeout(2000);
114
+
115
+ try {
110
116
  const modal2 = await detectConsentModal(session2.page, this.options);
111
117
  const acceptButton = modal2.buttons.find((b) => b.type === "accept");
112
118
 
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="WEB_MODULE" version="4">
3
- <component name="NewModuleRootManager">
4
- <content url="file://$MODULE_DIR$" />
5
- <orderEntry type="inheritedJdk" />
6
- <orderEntry type="sourceFolder" forTests="false" />
7
- </component>
8
- </module>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/gdpr-report.iml" filepath="$PROJECT_DIR$/.idea/gdpr-report.iml" />
6
- </modules>
7
- </component>
8
- </project>
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="" vcs="Git" />
5
- </component>
6
- </project>