implemented full response endpoint

This commit is contained in:
daniel muniz
2026-04-23 17:41:18 -03:00
parent cd15760e84
commit 69ea828968
7 changed files with 387 additions and 76 deletions

View File

@@ -125,6 +125,20 @@ function mapCategoryId(netstarId) {
- **Input**: `{"fqdn": "example.com"}` - **Input**: `{"fqdn": "example.com"}`
- **Output**: Full category info with reputation score, age rating, primary/secondary categories, and human-readable names - **Output**: Full category info with reputation score, age rating, primary/secondary categories, and human-readable names
- **Use Case**: Comprehensive categorization for security decisions - **Use Case**: Comprehensive categorization for security decisions
- **Includes**: `result` array matching the `/` endpoint format
### `POST /full` - Complete Raw Categorization Output
- **Input**: `{"fqdn": "example.com"}`
- **Output**: Complete detailed structure with all 35 fields from NetStar including:
- Primary, secondary, and security categories (with IDs, names, and mapped IDs)
- Reputation score and name
- Matching flags and their descriptions
- Age rating score and name
- All 9 category group classifications (Internet/Infrastructure, Malware/Security, Dangerous/Harmful, Adult, Business/Government, Personal, Computing/Technology, Social Media, Miscellaneous)
- Volume index
- Submitted URL
- **Use Case**: Complete diagnostic and analysis when all categorization data is needed
- **Includes**: `result` array matching the `/` endpoint format
### UDP Server (Port 33333) ### UDP Server (Port 33333)
- **Input**: Raw domain string (e.g., `"example.com"`) - **Input**: Raw domain string (e.g., `"example.com"`)

View File

@@ -12,7 +12,7 @@ module.exports = async function app(domain){
console.log(category) console.log(category)
const categoryConverted = await categoryConverter.execute(category) const categoryConverted = await categoryConverter.execute(category)
console.log(categoryConverted) console.log(categoryConverted)
return categoryConverted return [categoryConverted]
} }

View File

@@ -6,6 +6,7 @@ const bodyParser = require('body-parser')
const cron = require('./cron') const cron = require('./cron')
const express = require('express') const express = require('express')
const { ParseDetailedCategoryUseCase } = require('./use-cases/parse-detailed-category-use-case') const { ParseDetailedCategoryUseCase } = require('./use-cases/parse-detailed-category-use-case')
const { ParseFullCategoryUseCase } = require('./use-cases/parse-full-category-use-case')
const { CategoryConverterUseCase } = require('./use-cases/category-converter-use-case') const { CategoryConverterUseCase } = require('./use-cases/category-converter-use-case')
const categoriesMapping = require('./etc/categories-mapping.json') const categoriesMapping = require('./etc/categories-mapping.json')
@@ -19,7 +20,8 @@ const HTTP_PORT = process.env.HTTP_PORT
// Initialize use cases // Initialize use cases
const categoryConverter = new CategoryConverterUseCase({ categoriesMapping }) const categoryConverter = new CategoryConverterUseCase({ categoriesMapping })
const parseDetailedCategory = new ParseDetailedCategoryUseCase({ categoryConverter }) const parseDetailedCategory = new ParseDetailedCategoryUseCase({ categoryConverter })
const parseFullCategory = new ParseFullCategoryUseCase({ categoryConverter })
const server = dgram.createSocket('udp4'); const server = dgram.createSocket('udp4');
@@ -90,6 +92,19 @@ httpServer.post('/detailed', async (req, res) => {
} }
}) })
httpServer.post('/full', async (req, res) => {
try {
const { fqdn } = req.body
const fullResult = await parseFullCategory.execute(fqdn)
res.status(200).json(fullResult)
} catch (err) {
console.error('Error in /full endpoint:', err)
res.status(500).json({ error: err.message || err })
}
})
httpServer.listen(HTTP_PORT, () => { httpServer.listen(HTTP_PORT, () => {
console.log('HTTP server listening 3333') console.log('HTTP server listening 3333')
console.log('UDP server listening 3334') console.log('UDP server listening 3334')

View File

@@ -11,7 +11,7 @@ class CategoryConverterUseCase {
execute(category) { execute(category) {
const entry = this.categoriesMapping.find(item => item.id === category); const entry = this.categoriesMapping.find(item => item.id === category);
return entry ? entry.related[0].split(', ').map(str => str.trim()) : null; return entry ? entry.related[0] : null;
} }
} }

View File

@@ -36,77 +36,48 @@ class ParseDetailedCategoryUseCase {
quotedStrings.push(match[1]); quotedStrings.push(match[1]);
} }
// Split on whitespace // Split on tabs (primary separator in NetStar output)
const parts = output.trim().split(/\t|\s{2,}/); const parts = output.trim().split('\t');
let quotedIndex = 0;
// Find numeric IDs by looking for numbers that appear in sequence // Position mapping based on NetStar output structure:
// After "Categorized", we have: count, primary_id, secondary_id, ..., reputation_score, ..., age_rating_score // parts[0]: Categorized
// parts[1]: Matching Index
// parts[2]: Primary Category ID
// parts[3]: Primary Category Name (quoted)
// parts[4]: Secondary Category ID
// parts[5]: Secondary Category Name (quoted if secondary exists)
// parts[6]: Security Category ID
// parts[7]: Security Category Name
// parts[8]: Reputation Score ID
// parts[9]: Reputation Score Name (quoted)
// parts[10]: Matching Flag Value
// parts[11]: Matching Flag Names
// parts[12]: Age Rating ID
// parts[13]: Age Rating Name (quoted)
// Primary ID is parts[2] (always third element after "Categorized" and count) const primaryId = parseInt(parts[2]);
const primary = parts[2]; const primaryName = quotedStrings[quotedIndex++];
const primaryName = quotedStrings[0]; const primaryMapped = this.categoryConverter.execute(String(primaryId));
// Secondary ID is parts[4] (always fifth element) const secondaryId = parts[4] !== '0' && parts[4] !== '-' ? parseInt(parts[4]) : null;
const secondary = parts[4]; let secondaryName = null;
let secondaryMapped = null;
let secondaryName = ''; if (secondaryId !== null) {
let quotedIndex = 1; // Start after primary name secondaryName = quotedStrings[quotedIndex++];
secondaryMapped = this.categoryConverter.execute(String(secondaryId));
// If secondary is not "0", it has a quoted name
if (secondary !== '0' && secondary !== '-') {
secondaryName = quotedStrings[quotedIndex];
quotedIndex++;
} else { } else {
secondaryName = parts[5] === '-' ? '-' : parts[5]; secondaryName = parts[5] === '-' ? null : parts[5];
} }
// Find reputation score - it's a single digit that comes after some markers const reputationId = parseInt(parts[8]);
// Look for the pattern: a digit followed by a quoted reputation name const reputationName = quotedStrings[quotedIndex++];
let reputation = '';
let reputationName = '';
// Scan from parts[7] onwards to find reputation (it's usually around parts[8-11]) const ageRatingId = parseInt(parts[12]);
for (let i = 7; i < parts.length; i++) { const ageRatingName = quotedStrings[quotedIndex++];
const part = parts[i];
// Look for single/double digit that's not a category ID
if (!isNaN(part) && part !== '-' && !part.includes('x') && !part.includes('|')) {
const num = parseInt(part);
// Reputation scores are typically 0-5, single digit
if (num >= 0 && num <= 5 && parts[i+1] !== '-' && !parts[i+1].includes('0x')) {
reputation = part;
reputationName = quotedStrings[quotedIndex];
quotedIndex++;
break;
}
}
}
// Find age rating - it's another single digit that comes after more markers // Build result array in the same format as the / endpoint
// After reputation, we should find age rating
let ageRating = '';
let ageRatingName = '';
for (let i = 12; i < parts.length; i++) {
const part = parts[i];
if (!isNaN(part) && part !== '-' && !part.includes('x') && !part.includes('|')) {
const num = parseInt(part);
if (num >= 0 && num <= 5) {
// Make sure it's not already used as reputation
if (part !== reputation || i > 10) {
ageRating = part;
ageRatingName = quotedStrings[quotedIndex];
break;
}
}
}
}
// Convert category IDs using the mapper
const primaryMapped = this.categoryConverter.execute(primary);
const secondaryMapped = secondary !== '-' && secondary !== '0' ?
this.categoryConverter.execute(secondary) : null;
// Build result array in the same format as the / endpoint (as strings)
const resultArray = [String(primaryMapped)]; const resultArray = [String(primaryMapped)];
if (secondaryMapped !== null) { if (secondaryMapped !== null) {
resultArray.push(String(secondaryMapped)); resultArray.push(String(secondaryMapped));
@@ -118,9 +89,9 @@ class ParseDetailedCategoryUseCase {
primary_name: primaryName, primary_name: primaryName,
secondary: secondaryMapped, secondary: secondaryMapped,
secondary_name: secondaryName, secondary_name: secondaryName,
reputation: parseInt(reputation), reputation: reputationId,
reputation_name: reputationName, reputation_name: reputationName,
age_rating: parseInt(ageRating), age_rating: ageRatingId,
age_rating_name: ageRatingName, age_rating_name: ageRatingName,
raw_output: output.trim() raw_output: output.trim()
}; };

View File

@@ -0,0 +1,172 @@
const { exec } = require("node:child_process")
class ParseFullCategoryUseCase {
constructor({ categoryConverter }) {
this.categoryConverter = categoryConverter;
}
execute(domain) {
return new Promise((resolve, reject) => {
exec(`echo ${domain} | bin/gcf1check.sh etc check_categorize_hybrid`,
{ cwd: '/usr/local/gcf1' },
(error, stdout, stderr) => {
if (error) {
console.error(error);
reject(error);
return;
}
try {
const parsed = this.parseOutput(stdout);
resolve(parsed);
} catch (parseError) {
console.error('Parse error:', parseError);
reject(parseError);
}
});
});
}
parseOutput(output) {
const quotedStrings = [];
const quoteRegex = /"([^"]*)"/g;
let match;
while ((match = quoteRegex.exec(output)) !== null) {
quotedStrings.push(match[1]);
}
const parts = output.trim().split('\t');
let quotedIndex = 0;
// Extract primary category (parts[2], parts[3])
const primaryId = parseInt(parts[2]);
const primaryName = quotedStrings[quotedIndex++];
const primaryMapped = this.categoryConverter.execute(String(primaryId));
// Extract secondary category (parts[4], parts[5])
const secondaryId = parts[4] !== '0' && parts[4] !== '-' ? parseInt(parts[4]) : null;
let secondaryName = null;
let secondaryMapped = null;
if (secondaryId !== null && parts[5] !== '-') {
secondaryName = quotedStrings[quotedIndex++];
secondaryMapped = this.categoryConverter.execute(String(secondaryId));
} else {
secondaryName = parts[5] === '-' ? null : parts[5];
}
// Extract security category (parts[6], parts[7])
const securityId = parts[6] !== '0' && parts[6] !== '-' ? parseInt(parts[6]) : null;
let securityName = null;
let securityMapped = null;
if (securityId !== null && parts[7] !== '-') {
securityName = quotedStrings[quotedIndex++];
securityMapped = this.categoryConverter.execute(String(securityId));
} else {
securityName = parts[7] === '-' ? null : parts[7];
}
// Extract reputation (parts[8], parts[9])
const reputationId = parseInt(parts[8]);
const reputationName = quotedStrings[quotedIndex++];
// Extract matching flag (parts[10], parts[11])
const matchingFlagValue = parts[10];
const matchingFlagNames = parts[11] ? parts[11].split('|') : [];
// Extract age rating (parts[12], parts[13])
const ageRatingId = parseInt(parts[12]);
const ageRatingName = quotedStrings[quotedIndex++];
// Extract category groups starting from parts[16] (after 2 empty fields)
// Each group has: id (number), name (quoted string)
// Pattern: 0 "Internet/Infrastructure" 0 "Malware/Security" ...
const categoryGroups = {
internet_infrastructure: {
id: parseInt(parts[16]) || 0,
name: quotedStrings[quotedIndex++]
},
malware_security: {
id: parseInt(parts[18]) || 0,
name: quotedStrings[quotedIndex++]
},
dangerous_harmful: {
id: parseInt(parts[20]) || 0,
name: quotedStrings[quotedIndex++]
},
adult: {
id: parseInt(parts[22]) || 0,
name: quotedStrings[quotedIndex++]
},
business_government: {
id: parseInt(parts[24]) || 0,
name: quotedStrings[quotedIndex++]
},
personal: {
id: parseInt(parts[26]) || 0,
name: quotedStrings[quotedIndex++]
},
computing_technology: {
id: parseInt(parts[28]) || 0,
name: quotedStrings[quotedIndex++]
},
social_media: {
id: parseInt(parts[30]) || 0,
name: quotedStrings[quotedIndex++]
},
miscellaneous: {
id: parseInt(parts[32]) || 0,
name: quotedStrings[quotedIndex++]
}
};
// Extract volume index and submitted URL
const volumeIndex = parts[35];
const submittedUrl = parts[36];
const result = {
result_status: parts[0],
matching_index: parseInt(parts[1]),
primary_category: {
id: primaryId,
name: primaryName,
mapped_id: String(primaryMapped)
},
secondary_category: {
id: secondaryId,
name: secondaryName,
mapped_id: secondaryMapped ? String(secondaryMapped) : null
},
security_category: {
id: securityId,
name: securityName,
mapped_id: securityMapped ? String(securityMapped) : null
},
reputation: {
id: reputationId,
name: reputationName
},
matching_flag: {
value: matchingFlagValue,
names: matchingFlagNames
},
age_rating: {
id: ageRatingId,
name: ageRatingName
},
category_groups: categoryGroups,
volume_index: volumeIndex,
submitted_url: submittedUrl
};
// Add result array in the same format as / endpoint
const resultArray = [result.primary_category.mapped_id];
if (result.secondary_category.mapped_id !== null) {
resultArray.push(result.secondary_category.mapped_id);
}
result.result = resultArray;
return result;
}
}
module.exports = { ParseFullCategoryUseCase }

View File

@@ -1,14 +1,27 @@
@baseUrl = http://localhost:3333 @baseUrl = http://localhost:3333
### Test detailed endpoint with Facebook # ============================================
POST {{baseUrl}}/detailed # POST / - Basic Categorization Endpoint
# Returns: Simple result array of category IDs
# ============================================
### Basic endpoint - Facebook
POST {{baseUrl}}/
Content-Type: application/json Content-Type: application/json
{ {
"fqdn": "facebook.com" "fqdn": "facebook.com"
} }
### Test detailed endpoint with Google ### Basic endpoint - Google
POST {{baseUrl}}/
Content-Type: application/json
{
"fqdn": "google.com"
}
### Basic endpoint - TikTok
POST {{baseUrl}}/ POST {{baseUrl}}/
Content-Type: application/json Content-Type: application/json
@@ -16,15 +29,60 @@ Content-Type: application/json
"fqdn": "tiktok.com" "fqdn": "tiktok.com"
} }
### Test detailed endpoint with Reddit ### Basic endpoint - YouTube
POST {{baseUrl}}/detailed POST {{baseUrl}}/
Content-Type: application/json
{
"fqdn": "youtube.com"
}
### Basic endpoint - Reddit
POST {{baseUrl}}/
Content-Type: application/json Content-Type: application/json
{ {
"fqdn": "reddit.com" "fqdn": "reddit.com"
} }
### Test detailed endpoint with YouTube ### Basic endpoint - Twitter
POST {{baseUrl}}/
Content-Type: application/json
{
"fqdn": "twitter.com"
}
# ============================================
# POST /detailed - Detailed Categorization
# Returns: Categories with names, reputation, age rating
# ============================================
### Detailed endpoint - Facebook
POST {{baseUrl}}/detailed
Content-Type: application/json
{
"fqdn": "facebook.com"
}
### Detailed endpoint - Google
POST {{baseUrl}}/detailed
Content-Type: application/json
{
"fqdn": "google.com"
}
### Detailed endpoint - TikTok
POST {{baseUrl}}/detailed
Content-Type: application/json
{
"fqdn": "tiktok.com"
}
### Detailed endpoint - YouTube
POST {{baseUrl}}/detailed POST {{baseUrl}}/detailed
Content-Type: application/json Content-Type: application/json
@@ -32,7 +90,15 @@ Content-Type: application/json
"fqdn": "youtube.com" "fqdn": "youtube.com"
} }
### Test detailed endpoint with Twitter ### Detailed endpoint - Reddit
POST {{baseUrl}}/detailed
Content-Type: application/json
{
"fqdn": "reddit.com"
}
### Detailed endpoint - Twitter
POST {{baseUrl}}/detailed POST {{baseUrl}}/detailed
Content-Type: application/json Content-Type: application/json
@@ -40,10 +106,83 @@ Content-Type: application/json
"fqdn": "twitter.com" "fqdn": "twitter.com"
} }
### Test simple endpoint with Facebook (original endpoint) # ============================================
POST {{baseUrl}}/ # POST /full - Complete Raw Output
# Returns: All 35 fields from NetStar with category groups
# ============================================
### Full endpoint - Facebook
POST {{baseUrl}}/full
Content-Type: application/json Content-Type: application/json
{ {
"fqdn": "facebook.com" "fqdn": "facebook.com"
} }
### Full endpoint - Google
POST {{baseUrl}}/full
Content-Type: application/json
{
"fqdn": "google.com"
}
### Full endpoint - TikTok
POST {{baseUrl}}/full
Content-Type: application/json
{
"fqdn": "tiktok.com"
}
### Full endpoint - YouTube
POST {{baseUrl}}/full
Content-Type: application/json
{
"fqdn": "youtube.com"
}
### Full endpoint - Reddit
POST {{baseUrl}}/full
Content-Type: application/json
{
"fqdn": "reddit.com"
}
### Full endpoint - Twitter
POST {{baseUrl}}/full
Content-Type: application/json
{
"fqdn": "twitter.com"
}
# ============================================
# Edge cases and special domains
# ============================================
### Basic endpoint - Unknown domain
POST {{baseUrl}}/
Content-Type: application/json
{
"fqdn": "unknowndomainexample12345.com"
}
### Detailed endpoint - Localhost
POST {{baseUrl}}/detailed
Content-Type: application/json
{
"fqdn": "localhost"
}
### Full endpoint - IP-like domain
POST {{baseUrl}}/full
Content-Type: application/json
{
"fqdn": "99999.incompass.netstar-inc.com"
}