/*****************************************************************************/ /* CSPreport.c Content Security Policy Report[er] https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP When POSTed to, this utility appends a timestamp and CSP report JSON to the file specified by the CSPREPORT_FILE logical name. This file must be located somewhere the scripting account has read+write access to. When accessed using a GET the utility accesses the stored CSP reports and returns a formatted HTML report listing each. GET requests (reporting) must be subject to authentication and authorisation. WARNING: depending on the CSP options selected and applied a busy site can generate *LOTS* of report entries! Example mapping configuration for POSTing violation reports. # WASD_CONFIG_MAP set * response=csp="img-src the.host.name; \ report-uri /cgi-bin/cspreport;" And for the CSP "report-only" option. # WASD_CONFIG_MAP set * response=cspro="default-src the.host.name; \ report-uri /cgi-bin/cspreport;" The utility can be run as a CGI or CGIplus script. /cgi-bin/cspreport CGIplus is more responsive and efficient. /cgiplus-bin/cspreport When using CGIplus don't forget HTTPD/DO=DCL=DELETE to restart script. The report file name may contain "YYYY" to have the year substituted, "MM" the month, "DD" the day, and "HH" the hour substituted as digits. For example, $ DEFINE /SYSTEM CSPREPORT_FILE "LOGS:CSPREPORT_yyyymmdd.LOG" would record all CSP reports for the day in a single file. Maximum granularity is one hour. Using a browser and accessing the script with no trailing path provides a report of the current file's content. /cgi-bin/cspreport A wildcard trailing path provides a clickable list of the current file names. /cgi-bin/cspreport/* A file name path provides the content of that file. /cgi-bin/cspreport/cspreport_20200216.log LOGICAL NAMES ------------- CSPREPORT_DBUG turns on all "if (dbug)" statements CSPREPORT_FILE locates the report storage file CSPREPORT_MAX integer defines maximum report size accepted BUILD DETAILS ------------- $ @BUILD_CSPREPORT $ @BUILD_CSPREPORT LINK COPYRIGHT --------- Copyright (C) 2020,2021 Mark G.Daniel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. VERSION HISTORY --------------- 15-FEB-2020 MGD v1.0.0, initial development */ /*****************************************************************************/ #define SOFTWAREVN "1.0.0" #define SOFTWARENM "CSPREPORT" #ifdef __ALPHA # define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN #endif #ifdef __ia64 # define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN #endif #ifdef __VAX # error VAX no longer implemented #endif #ifdef __x86_64 # define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN #endif /* standard C header files */ #include #include #include #include #include #include /* VMS header files */ #include #include #include #include /* application related header file */ #include "../misc/cgilib.h" #define REPORT_SIZE_MAX 4096 /* bytes */ /* global storage */ int dbug, IsCgiPlus, ReportSizeMax = REPORT_SIZE_MAX; unsigned long Delta100mS [2] = { -1000000, -1 }; unsigned long BinTime [2]; unsigned short NumTime [7]; char *ReportFilePtr; void OutputReport (char*); void PostReport (void); void ProcessRequest (void); char* ReportFileName (void); int ReportList (char*); /*****************************************************************************/ /* */ main (int argc, char* argv[]) { char *cptr; /*********/ /* begin */ /*********/ if (dbug = (getenv ("CSPREPORT_DBUG") != NULL)) { CgiLibEnvironmentSetDebug (dbug); fprintf (stdout, "Content-Type: text/plain\r\n\r\n"); } if (!(ReportFilePtr = getenv ("CSPREPORT_FILE"))) { CgiLibResponseHeader (500, "text/plain"); fprintf (stdout, "CSPREPORT_FILE not defined!\n"); return; } if (cptr = getenv ("CSPREPORT_MAX")) { ReportSizeMax = atol(cptr); if (ReportSizeMax <= 0) ReportSizeMax = REPORT_SIZE_MAX; } CgiLibEnvironmentInit (argc, argv, 0); IsCgiPlus = CgiLibEnvironmentIsCgiPlus (); if (IsCgiPlus) { for (;;) { /* block waiting for the next request */ CgiLibVar (""); ProcessRequest (); CgiLibCgiPlusEOF (); } } else ProcessRequest (); } /*****************************************************************************/ /* */ void ProcessRequest (void) { char *cptr; /*********/ /* begin */ /*********/ if (dbug) fprintf (stdout, "ProcessRequest()\n"); sys$gettim (&BinTime); sys$numtim (&NumTime, &BinTime); cptr = CgiLibVar ("WWW_REQUEST_METHOD"); if (!strcmp (cptr, "POST")) PostReport (); else if (!strcmp (cptr, "GET")) { cptr = CgiLibVar ("WWW_PATH_INFO"); if (strchr (cptr,'*')) { if (!ReportList (cptr)) OutputReport (NULL); } else OutputReport (cptr); } else { CgiLibResponseHeader (403, "text/plain"); fprintf (stdout, "Hmmm...\n"); return; } } /*****************************************************************************/ /* Append the JSON formatted report POSTed by the browser. */ void PostReport (void) { int retry, Context, PostBufferCount; char *cptr, *VariablePtr, *PostBufferPtr; FILE *rfp; /*********/ /* begin */ /*********/ if (dbug) fprintf (stdout, "PostReport()\n"); cptr = CgiLibVar ("WWW_CONTENT_LENGTH"); if (atol(cptr) > ReportSizeMax) { CgiLibResponseHeader (400, "text/plain"); fprintf (stdout, "Report exceeds maximum size!\n"); return; } /* read the POSTed request body */ CgiLibReadRequestBody (&PostBufferPtr, &PostBufferCount); if (PostBufferPtr == NULL || PostBufferCount == 0) { CgiLibResponseHeader (400, "text/plain"); fprintf (stdout, "No report in the POST!\n"); return; } cptr = CgiLibVar ("WWW_CONTENT_TYPE"); if (!strcmp (cptr, "application/csp-report")) { /* try to open the file for 10 seconds */ for (retry = 100; retry; retry--) { rfp = fopen (ReportFileName(), "a+", "shr=get,put"); if (rfp) break; if (vaxc$errno != RMS$_FLK) break; sys$schdwk (0, 0, &Delta100mS, 0); sys$hiber (); continue; } if (!rfp) { CgiLibResponseHeader (500, "text/plain"); fprintf (stdout, "Report fopen() %%X%08.08X!\n", vaxc$errno); } else { fprintf (rfp, "%04.04u-%02.02u-%02.02u %02.02u:%02.02u:%02.02u\n%s\n", NumTime[0], NumTime[1], NumTime[2], NumTime[3], NumTime[4], NumTime[5], PostBufferPtr); fclose (rfp); } CgiLibResponseHeader (200, "text/plain", "Content-Length: 0\r\n"); } else { CgiLibResponseHeader (400, "text/plain"); fprintf (stdout, "Content not \"application/csp-report\"!\n"); } free (PostBufferPtr); } /*****************************************************************************/ /* If |fname| is NULL or empty return the content of the current report file, otherwise the content of the specified file name (nam, not fill specification). */ void OutputReport (char *fname) { int retry; char *cptr, *sptr, *zptr; char line [1024], FileName [256]; FILE *rfp; /*********/ /* begin */ /*********/ if (dbug) fprintf (stdout, "OutputReport() |%s|\n", fname); if (!CgiLibVar ("WWW_REMOTE_USER")[0]) { CgiLibResponseHeader (403, "text/plain"); fprintf (stdout, "Must be authenticated!\n"); return; } if (fname && fname[0] && fname[0] != '/') { zptr = (sptr = FileName) + sizeof(FileName)-1; for (cptr = ReportFileName(); *cptr && sptr < zptr; *sptr++ = *cptr++); if (sptr > FileName) sptr--;; while (sptr > FileName && *sptr != ']' && *sptr != ':') sptr--; if (sptr > FileName) sptr++; if (*fname == '/') fname++; for (cptr = fname; *cptr && sptr < zptr; *sptr++ = *cptr++); *sptr = '\0'; fname = FileName; } else fname = ReportFileName(); /* try to open the file for 10 seconds */ for (retry = 100; retry; retry--) { rfp = fopen (fname, "r", "shr=get,put"); if (rfp) break; if (vaxc$errno != RMS$_FLK) break; sys$schdwk (0, 0, &Delta100mS, 0); sys$hiber (); continue; } if (!rfp) { CgiLibResponseHeader (500, "text/plain"); fprintf (stdout, "Report fopen() %s %s.\n", fname, strerror(errno)); return; } CgiLibResponseHeader (200, "text/html"); fprintf (stdout, "\n\n\n\n\n
\n%s %s\n",
            SOFTWAREID, fname);
   while (fgets (line, sizeof(line), rfp))
   {
      for (cptr = line; *cptr; cptr++);
      if (cptr > line && *(cptr-1) == '\n') *(cptr-1) = '\0';
      if (line[0] == '{')
         fprintf (stdout,
"\n",
                  line);
      else   
         fprintf (stdout, "%s ", line);
   }
   fprintf (stdout, "\n\
\n\n\n"); fclose (rfp); } /*****************************************************************************/ /* Generate a clickable list of file names containing CSP reports. */ int ReportList (char *wcard) { int count, status; char *aptr, *cptr, *sptr; char size [32], FileName [256], FileSpec [256], ExpFileName [256]; struct stat stbuf; struct FAB SearchFab; struct NAM SearchNam; /*********/ /* begin */ /*********/ if (dbug) fprintf (stdout, "ReportList() |%s|\n", wcard); strcpy (FileSpec, ReportFilePtr); if (!(sptr = strstr (FileSpec, "YYYY"))) sptr = strstr (FileSpec, "yyyy"); if (!sptr) return (0); for (cptr = sptr; *cptr; cptr++); if (cptr > sptr) cptr--; while (cptr > sptr && *cptr != '.') cptr--; if (cptr <= sptr) return (0); /* modify the "YYYY" into a wildcard and move the file type up */ *sptr++ = '*'; while (*cptr) *sptr++ = *cptr++; *sptr = '\0'; if (dbug) fprintf (stdout, "|%s|\n", FileSpec); /* initialize the file access block */ SearchFab = cc$rms_fab; SearchFab.fab$l_fna = FileSpec; SearchFab.fab$b_fns = strlen(FileSpec); SearchFab.fab$l_nam = &SearchNam; SearchNam = cc$rms_nam; SearchNam.nam$l_esa = ExpFileName; SearchNam.nam$b_ess = sizeof(ExpFileName)-1; SearchNam.nam$l_rsa = FileName; SearchNam.nam$b_rss = sizeof(FileName)-1; if ((status = sys$parse (&SearchFab, 0, 0)) & 1 == 0) { CgiLibResponseHeader (500, "text/plain"); fprintf (stdout, "Report $parse() %s %%X%08.08X.\n", FileSpec, status); return (-1); } if (!(sptr = CgiLibVar ("WWW_SCRIPT_NAME"))) { CgiLibResponseHeader (500, "text/plain"); fprintf (stdout, "Script name?\n"); return (-1); } count = 0; while ((status = sys$search (&SearchFab, 0, 0)) & 1) { *SearchNam.nam$l_ver = '\0'; if (dbug) fprintf (stdout, "FileName |%s|\n", FileName); if (stat (FileName, &stbuf)) { status = vaxc$errno; break; } if (!count) { CgiLibResponseHeader (200, "text/html"); fprintf (stdout, "\n\n\n\n\n
\n%s %s\n",
                  SOFTWAREID, FileSpec);
      }
      count++;
      if (stbuf.st_size < 1000)
         sprintf (size, "%uB", stbuf.st_size);
      else
      if (stbuf.st_size < 1000000)
         sprintf (size, "%ukB", stbuf.st_size / 1000);
      else
      if (stbuf.st_size < 1000000000)
         sprintf (size, "%uMB", stbuf.st_size / 1000000);
      else
         sprintf (size, "%uGB", stbuf.st_size / 1000000000);
      fprintf (stdout, "%s %s\n",
               sptr, SearchNam.nam$l_name, SearchNam.nam$l_name, size);
      *SearchNam.nam$l_ver = ';';
   }

   if (dbug) fprintf (stdout, "%%X%08.08X\n", status);
   if (status != RMS$_FNF && status != RMS$_NMF)
   {
      if (!count) CgiLibResponseHeader (500, "text/plain");
      fprintf (stdout, "Report $parse() %s %%X%08.08X.\n",
               FileSpec, status);
      return (-1);
   }

   if (count) fprintf (stdout, "
\n\n\n"); return (count); } /*****************************************************************************/ /* Return a pointer to the report (possibly time-stamped) file name. If the file name contains "YYYY" then add time components at those locations. */ char* ReportFileName (void) { static int PrevDay = -1, PrevHour = -1; static char FileName [256]; static char *year, *month, *day, *hour; char buf [32]; /*********/ /* begin */ /*********/ if (dbug) fprintf (stdout, "ReportFileName() |%s|%s|\n", ReportFilePtr, FileName); /* minimum report file granularity is one hour */ if (NumTime[3] == PrevHour && NumTime[2] == PrevDay) return (FileName); PrevHour = NumTime[3]; PrevDay = NumTime[2]; /* (re)build the file name */ strcpy (FileName, ReportFilePtr); year = month = day = hour = NULL; if (!(year = strstr (FileName, "YYYY"))) year = strstr (FileName, "yyyy"); if (year) { sprintf (buf, "%4.4u", NumTime[0]); year[0] = buf[0]; year[1] = buf[1]; year[2] = buf[2]; year[3] = buf[3]; if (!(month = strstr (FileName, "MM"))) month = strstr (FileName, "mm"); if (month) { sprintf (buf, "%02.02u", NumTime[1]); month[0] = buf[0]; month[1] = buf[1]; } if (!(day = strstr (FileName, "DD"))) day = strstr (FileName, "dd"); if (day) { sprintf (buf, "%02.02u", NumTime[2]); day[0] = buf[0]; day[1] = buf[1]; } if (!(hour = strstr (FileName, "HH"))) hour = strstr (FileName, "hh"); if (hour) { sprintf (buf, "%02.02u", NumTime[4]); hour[0] = buf[0]; hour[1] = buf[1]; } } if (dbug) fprintf (stdout, "|%s|\n", FileName); return (FileName); } /*****************************************************************************/