• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

statuscompliance / status-backend / 19856219812

02 Dec 2025 10:58AM UTC coverage: 91.137% (+2.5%) from 88.591%
19856219812

push

github

alvarobernal2412
refactor: fix bad smells

1450 of 1672 branches covered (86.72%)

Branch coverage included in aggregate %.

87 of 114 new or added lines in 5 files covered. (76.32%)

7 existing lines in 2 files now uncovered.

2735 of 2920 relevant lines covered (93.66%)

27.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.1
/src/controllers/computation.controller.js
1
import { models } from '../models/models.js';
2
import { Op , Sequelize} from 'sequelize';
3
import { checkRequiredProperties } from '../utils/checkRequiredProperties.js';
4
import nodered from '../config/nodered.js';
5
import { v4 as uuidv4 } from 'uuid';
6
import redis from '../config/redis.js';
7
import { calculateCompliance } from '../utils/calculateCompliance.js';
8
import { handleControllerError } from '../utils/errorHandler.js';
9

10
const isTestEnvironment = !!import.meta.env?.VITEST;
78✔
11
const API_PREFIX = isTestEnvironment ? '' : process.env.API_PREFIX;
78!
12

13
/**
14
 * Validates that the endpoint doesn't contain malicious patterns.
15
 * 
16
 * Security measures:
17
 * - Prevents path traversal attacks (../)
18
 * - Blocks URL encoding bypass attempts (%2e, %2f, %5c)
19
 * - Rejects null bytes and special characters
20
 * - Ensures clean path format (no backslashes, double slashes)
21
 * - Restricts to safe character set: alphanumeric, hyphens, underscores, slashes
22
 * 
23
 * @param {string} endpoint - The endpoint to validate
24
 * @returns {boolean} - True if valid, false otherwise
25
 */
26
function isValidNodeRedEndpoint(endpoint) {
27
  if (!endpoint || typeof endpoint !== 'string') {
8!
UNCOV
28
    return false;
×
29
  }
30
  
31
  // Ensure it starts with /
32
  if (!endpoint.startsWith('/')) {
8!
UNCOV
33
    return false;
×
34
  }
35
  
36
  // Prevent path traversal attempts
37
  if (endpoint.includes('..')) {
8✔
38
    return false;
1✔
39
  }
40
  
41
  // Prevent backslashes (use forward slashes only)
42
  if (endpoint.includes('\\')) {
7!
UNCOV
43
    return false;
×
44
  }
45
  
46
  // Prevent null bytes
47
  if (endpoint.includes('\0') || endpoint.includes('%00')) {
7!
UNCOV
48
    return false;
×
49
  }
50
  
51
  // Prevent URL encoding that could be used to bypass filters
52
  if (endpoint.match(/%2e|%2f|%5c/i)) {
7!
UNCOV
53
    return false;
×
54
  }
55
  
56
  // Only allow alphanumeric, hyphens, underscores, and forward slashes
57
  if (!/^[a-zA-Z0-9/_-]+$/.test(endpoint)) {
7✔
58
    return false;
1✔
59
  }
60
  
61
  // Prevent multiple consecutive slashes
62
  if (endpoint.includes('//')) {
6!
UNCOV
63
    return false;
×
64
  }
65
  
66
  return true;
6✔
67
}
68

69
export async function getComputations(req, res) {
70
  try {
4✔
71
    const computations = await models.Computation.findAll();
4✔
72
    res.status(200).json(computations);
2✔
73
  } catch (error) {
74
    handleControllerError(res, error, 'Failed to get computations');
2✔
75
  }
76
}
77

78
export async function getComputationsById(req, res) {
79
  try {
8✔
80
    const { id } = req.params;
8✔
81
    const computations = await models.Computation.findAll({
8✔
82
      where: { computationGroup: id },
83
    });
84
    const ready = await redis.get(id);
6✔
85
    if (computations.length === 0) {
6✔
86
      return res.status(404).json({ message: 'Computations not found' });
2✔
87
    }
88
    if (ready !== 'true') {
4✔
89
      return res.status(202).json({ message: 'Not ready yet' });
2✔
90
    }
91
    return res.status(200).json({
2✔
92
      code: 200,
93
      message: 'OK',
94
      computations: calculateCompliance(computations),
95
    });
96
  } catch (error) {
97
    handleControllerError(res, error, 'Failed to get computation by ID');
2✔
98
  }
99
}
100

101
export async function getComputationsByControlId(req, res) {
102
  try {
4✔
103
    const { controlId } = req.params;
4✔
104
    const computations = await models.Computation.findAll({
4✔
105
      where: { controlId },
106
    });
107
    res.status(200).json(computations);
2✔
108
  } catch (error) {
109
    handleControllerError(res, error, 'Failed to get computations by control ID');
2✔
110
  }
111
}
112

113
export async function getComputationsByControlIdAndCreationDate(req, res) {
114
  try {
4✔
115
    const { controlId, createdAt } = req.params;
4✔
116

117
    const startOfDay = new Date(createdAt);
4✔
118
    startOfDay.setUTCHours(0, 0, 0, 0);
4✔
119

120
    const endOfDay = new Date(createdAt);
4✔
121
    endOfDay.setUTCHours(23, 59, 59, 999);
4✔
122

123
    const computations = await models.Computation.findAll({
4✔
124
      where: {
125
        controlId,
126
        createdAt: {
127
          [Op.between]: [startOfDay, endOfDay],
128
        },
129
      },
130
    });
131

132
    res.status(200).json(computations);
2✔
133
  } catch (error) {
134
    handleControllerError(res, error, 'Failed to get computation by control ID and creation date');
2✔
135
  }
136
}
137

138
export async function setComputeIntervalBytControlIdAndCreationDate(req, res) {
139
  try {
9✔
140
    const { controlId } = req.params;
9✔
141
    const { from, to } = req.body;
9✔
142

143
    if (!from || !to) {
9✔
144
      return res.status(400).json({ error: '"from" and "to" are required in body' });
4✔
145
    }
146
    const [updated] = await models.Computation.update(
5✔
147
      { period: { from, to } },
148
      {
149
        where: {
150
          controlId,
151
          [Op.and]: [
152
            // Use Sequelize.literal to explicitly extract the 'from' value as text
153
            // and then cast it to TIMESTAMPTZ for comparison.
154
            Sequelize.where(
155
              Sequelize.cast(Sequelize.literal("period->>'from'"), 'TIMESTAMPTZ'),
156
              { [Op.gte]: from }
157
            ),
158
            // Do the same for the 'to' value.
159
            Sequelize.where(
160
              Sequelize.cast(Sequelize.literal("period->>'to'"), 'TIMESTAMPTZ'),
161
              { [Op.lte]: to }
162
            )
163
          ]
164
        },
165
      }
166
    );
167
    if (updated === 0) {
4✔
168
      return res.status(404).json({ message: 'No computations found for the given controlId' });
2✔
169
    }
170

171
    return res.status(204).json({ message: `${updated} computations updated.` });
2✔
172
  } catch (error) {
173
    handleControllerError(res, error, 'Failed to update computation interval');
1✔
174
  }
175
};
176

177
export async function createComputation(req, res) {
178
  try {
12✔
179
    const { metric, config } = req.body;
12✔
180
    const { validation, textError } = checkRequiredProperties(metric, [
12✔
181
      'endpoint',
182
      'params',
183
    ]);
184
    if (!validation) {
12✔
185
      return res.status(400).json({ error: textError });
4✔
186
    }
187
    
188
    // Validate the endpoint against whitelist
189
    if (!isValidNodeRedEndpoint(metric.endpoint)) {
8✔
190
      return res.status(400).json({ 
2✔
191
        error: 'Invalid endpoint. The specified endpoint is not allowed.' 
192
      });
193
    }
194
    
195
    const endpoint = `/${API_PREFIX}${metric.endpoint}`;
6✔
196
    const computationId = uuidv4();
6✔
197
    const { end: to, ...restWindow } = metric.window;
6✔
198
    const params = {
6✔
199
      computationGroup: computationId,
200
      backendUrl: config.backendUrl,
201
      ...metric.params,
202
      scope: metric.scope,
203
      to,
204
      ...restWindow,
205
    };
206
    const headers = {
6✔
207
      'x-access-token': req.cookies.accessToken,
208
    };
209
    const response = await nodered.post(endpoint, params, { headers });
6✔
210
    if (response.status !== 200) {
4✔
211
      return res
2✔
212
        .status(400)
213
        .json({ message: 'Something went wrong when calling Node-RED' });
214
    }
215
    res.status(201).json({
2✔
216
      code: 201,
217
      message: 'OK',
218
      computation: `${API_PREFIX}/computations/${computationId}`,
219
    });
220
  } catch (error) {
221
    handleControllerError(res, error, 'Failed to create computation');
2✔
222
  }
223
}
224

225
export async function bulkCreateComputations(req, res) {
226
  try {
11✔
227
    const { computations, done } = req.body;
11✔
228
    if (!Array.isArray(computations) || computations.length === 0) {
11✔
229
      return res.status(400).json({ error: 'Invalid computations' });
4✔
230
    }
231
    const { validation, textError } = checkRequiredProperties(computations[0], [
7✔
232
      'computationGroup',
233
    ]);
234
    if (!validation) {
7✔
235
      return res.status(400).json({ error: textError });
2✔
236
    }
237
    const newComputations = await models.Computation.bulkCreate(computations);
5✔
238
    if (done) {
3✔
239
      const computationGroup = computations[0].computationGroup;
2✔
240
      await redis.set(computationGroup, true);
2✔
241
    }
242
    res.status(201).json(newComputations);
3✔
243
  } catch (error) {
244
    handleControllerError(res, error, 'Failed to create computations');
2✔
245
  }
246
}
247

248
export async function deleteComputations(req, res) {
249
  try {
4✔
250
    await models.Computation.destroy({ where: {} });
4✔
251
    res.status(204).end();
2✔
252
  } catch (error) {
253
    handleControllerError(res, error, 'Failed to delete computations');
2✔
254

255
  }
256
}
257

258
export async function deleteComputationByControlId(req, res) {
259
  try {
6✔
260
    const { controlId } = req.params;
6✔
261
    const deletedCount = await models.Computation.destroy({ where: { controlId } });
6✔
262
    if (deletedCount === 0) {
4✔
263
      return res.status(404).json({ message: 'No computations found to delete' });
2✔
264
    }
265
    res.status(204).end();
2✔
266
  } catch (error) {
267
    handleControllerError(res, error, 'Failed to delete computation by control ID');
2✔
268
  }
269
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc